mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 16:22:16 +01:00
Compare commits
1 Commits
feature/bi
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
979de8202e |
@@ -67,7 +67,7 @@ class TimeEntryController extends Controller
|
||||
|
||||
$query = TimeEntry::query()
|
||||
->where('organization_id', $organization->getKey())
|
||||
->where('member_id', $member->getKey())
|
||||
->where('user_id', $member->user_id)
|
||||
->when($exclude !== null, function (Builder $q) use ($exclude): void {
|
||||
$q->where('id', '!=', $exclude->getKey());
|
||||
})
|
||||
@@ -107,8 +107,8 @@ class TimeEntryController extends Controller
|
||||
/**
|
||||
* Get time entries in organization
|
||||
*
|
||||
* If you only need time entries for a specific user, you can filter by `member_id`.
|
||||
* Users with the permission `time-entries:view:own` can only use this endpoint with their own member ID in the member_id filter.
|
||||
* If you only need time entries for a specific user, you can filter by `user_id`.
|
||||
* Users with the permission `time-entries:view:own` can only use this endpoint with their own user ID in the user_id filter.
|
||||
*
|
||||
* @return TimeEntryCollection<TimeEntryResource>
|
||||
*
|
||||
@@ -118,17 +118,16 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
|
||||
{
|
||||
$member = $this->member($organization);
|
||||
/** @var Member|null $memberFilter */
|
||||
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($member !== null && $member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:view:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
}
|
||||
|
||||
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $memberFilter, $canAccessPremiumFeatures);
|
||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
|
||||
|
||||
$totalCount = $timeEntriesQuery->count();
|
||||
|
||||
@@ -159,7 +158,7 @@ class TimeEntryController extends Controller
|
||||
if ($timeEntries->count() === 0) {
|
||||
Log::warning('User has has more than '.$limit.' time entries on one date', [
|
||||
'date' => $lastDate->toDateString(),
|
||||
'member_id' => $request->input('member_id'),
|
||||
'user_id' => $request->input('user_id'),
|
||||
'auth_user_id' => Auth::id(),
|
||||
'limit' => $limit,
|
||||
]);
|
||||
@@ -222,10 +221,9 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
|
||||
{
|
||||
$member = $this->member($organization);
|
||||
/** @var Member|null $memberFilter */
|
||||
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($member !== null && $member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:view:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
@@ -242,7 +240,7 @@ class TimeEntryController extends Controller
|
||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||
|
||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $memberFilter, $canAccessPremiumFeatures);
|
||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
|
||||
$timeEntriesQuery->with([
|
||||
'task',
|
||||
'client',
|
||||
@@ -265,7 +263,7 @@ class TimeEntryController extends Controller
|
||||
if ($viewFile === false) {
|
||||
throw new \LogicException('View file not found');
|
||||
}
|
||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
|
||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
||||
$timeEntriesAggregateQuery,
|
||||
null,
|
||||
@@ -372,10 +370,9 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request, TimeEntryAggregationService $timeEntryAggregationService): array
|
||||
{
|
||||
$member = $this->member($organization);
|
||||
/** @var Member|null $memberFilter */
|
||||
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($member !== null && $member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:view:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
@@ -386,7 +383,7 @@ class TimeEntryController extends Controller
|
||||
|
||||
$group1Type = $request->getGroup();
|
||||
$group2Type = $request->getSubGroup();
|
||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
|
||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||
|
||||
@@ -422,10 +419,9 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
|
||||
{
|
||||
$member = $this->member($organization);
|
||||
/** @var Member|null $memberFilter */
|
||||
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($member !== null && $member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:view:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
@@ -441,7 +437,7 @@ class TimeEntryController extends Controller
|
||||
|
||||
$group = $request->getGroup();
|
||||
$subGroup = $request->getSubGroup();
|
||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
|
||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||
|
||||
@@ -584,7 +580,7 @@ class TimeEntryController extends Controller
|
||||
{
|
||||
/** @var Member $member */
|
||||
$member = Member::query()->findOrFail($request->input('member_id'));
|
||||
if ($member->getKey() === $this->member($organization)->getKey()) {
|
||||
if ($member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:create:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:create:all');
|
||||
@@ -631,10 +627,9 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource
|
||||
{
|
||||
$member = $this->member($organization);
|
||||
/** @var Member|null $newMember */
|
||||
$newMember = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($timeEntry->member_id === $member->getKey() && ($newMember === null || $newMember->getKey() === $member->getKey())) {
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($timeEntry->member->user_id === Auth::id() && ($member === null || $member->user_id === Auth::id())) {
|
||||
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry);
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
|
||||
@@ -666,10 +661,6 @@ class TimeEntryController extends Controller
|
||||
}
|
||||
|
||||
$timeEntry->fill($request->validated());
|
||||
if ($newMember !== null) {
|
||||
$timeEntry->member()->associate($newMember);
|
||||
$timeEntry->user()->associate($newMember->user);
|
||||
}
|
||||
$timeEntry->description = $request->input('description', $timeEntry->description) ?? '';
|
||||
$timeEntry->setComputedAttributeValue('billable_rate');
|
||||
$timeEntry->save();
|
||||
@@ -699,7 +690,6 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function updateMultiple(Organization $organization, TimeEntryUpdateMultipleRequest $request): JsonResponse
|
||||
{
|
||||
$member = $this->member($organization);
|
||||
$this->checkAnyPermission($organization, ['time-entries:update:all', 'time-entries:update:own']);
|
||||
$canAccessAll = $this->hasPermission($organization, 'time-entries:update:all');
|
||||
|
||||
@@ -724,9 +714,6 @@ class TimeEntryController extends Controller
|
||||
throw new AuthorizationException;
|
||||
}
|
||||
|
||||
/** @var Member|null $newMember */
|
||||
$newMember = isset($changes['member_id']) ? Member::query()->findOrFail($changes['member_id']) : null;
|
||||
|
||||
$project = null;
|
||||
$client = null;
|
||||
$overwriteClient = false;
|
||||
@@ -753,7 +740,7 @@ class TimeEntryController extends Controller
|
||||
|
||||
continue;
|
||||
}
|
||||
if (! $canAccessAll && $timeEntry->member_id !== $member->getKey()) {
|
||||
if (! $canAccessAll && $timeEntry->user_id !== Auth::id()) {
|
||||
$error->push($id);
|
||||
|
||||
continue;
|
||||
@@ -763,10 +750,6 @@ class TimeEntryController extends Controller
|
||||
$oldTask = $timeEntry->task;
|
||||
|
||||
$timeEntry->fill($changes);
|
||||
if ($newMember !== null) {
|
||||
$timeEntry->member()->associate($newMember);
|
||||
$timeEntry->user_id = $newMember->user_id;
|
||||
}
|
||||
// If project is changed, but task is not, we remove the old task from the time entry
|
||||
if ($oldProject !== null && $project !== null && $oldProject->isNot($project) && $task === null) {
|
||||
$timeEntry->task()->disassociate();
|
||||
@@ -807,8 +790,7 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
|
||||
{
|
||||
$member = $this->member($organization);
|
||||
if ($timeEntry->member_id === $member->getKey()) {
|
||||
if ($timeEntry->member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);
|
||||
@@ -865,7 +847,7 @@ class TimeEntryController extends Controller
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $canDeleteAll && $timeEntry->member_id !== $this->member($organization)->getKey()) {
|
||||
if (! $canDeleteAll && $timeEntry->user_id !== Auth::id()) {
|
||||
$error->push($id);
|
||||
|
||||
continue;
|
||||
|
||||
@@ -80,10 +80,6 @@ class PermissionStore
|
||||
'invoices:update',
|
||||
'invoices:download',
|
||||
'invoices:delete',
|
||||
'invoice-recipients:view',
|
||||
'invoice-recipients:create',
|
||||
'invoice-recipients:update',
|
||||
'invoice-recipients:delete',
|
||||
'invoice-settings:view',
|
||||
'invoice-settings:update',
|
||||
],
|
||||
@@ -151,10 +147,6 @@ class PermissionStore
|
||||
'invoices:update',
|
||||
'invoices:download',
|
||||
'invoices:delete',
|
||||
'invoice-recipients:view',
|
||||
'invoice-recipients:create',
|
||||
'invoice-recipients:update',
|
||||
'invoice-recipients:delete',
|
||||
'invoice-settings:view',
|
||||
'invoice-settings:update',
|
||||
],
|
||||
@@ -211,10 +203,6 @@ class PermissionStore
|
||||
'invoices:update',
|
||||
'invoices:download',
|
||||
'invoices:delete',
|
||||
'invoice-recipients:view',
|
||||
'invoice-recipients:create',
|
||||
'invoice-recipients:update',
|
||||
'invoice-recipients:delete',
|
||||
'invoice-settings:view',
|
||||
'invoice-settings:update',
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Billing": {
|
||||
"repository": "solidtime-io/extension-billing",
|
||||
"ref": "v0.0.4"
|
||||
"ref": "v0.0.1"
|
||||
},
|
||||
"Services": {
|
||||
"repository": "solidtime-io/extension-services",
|
||||
@@ -9,6 +9,6 @@
|
||||
},
|
||||
"Invoicing": {
|
||||
"repository": "solidtime-io/extension-invoicing",
|
||||
"ref": "feature/recipients"
|
||||
"ref": "v0.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +117,6 @@ export type DetailedInvoiceResponse = ZodiosResponseByAlias<SolidTimeApi, 'getIn
|
||||
export type DetailedInvoice = DetailedInvoiceResponse['data'];
|
||||
|
||||
export type InvoiceIndexEntry = ZodiosResponseByAlias<SolidTimeApi, 'getInvoices'>['data'][0];
|
||||
export type InvoiceRecipient = ZodiosResponseByAlias<SolidTimeApi, 'getInvoiceRecipients'>['data'][0];
|
||||
export type InvoiceRecipientBody = ZodiosBodyByAlias<SolidTimeApi, 'createInvoiceRecipient'>;
|
||||
|
||||
export type UpdateInvoiceSettings = ZodiosBodyByAlias<SolidTimeApi, 'updateInvoiceSettings'>;
|
||||
|
||||
|
||||
@@ -45,54 +45,14 @@ const InvitationResource = z
|
||||
const InvitationStoreRequest = z
|
||||
.object({ email: z.string().email(), role: z.enum(['admin', 'manager', 'employee']) })
|
||||
.passthrough();
|
||||
const InvoiceRecipientResource = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
organization_id: z.string(),
|
||||
name: z.string(),
|
||||
vatin: z.union([z.string(), z.null()]),
|
||||
address_line_1: z.union([z.string(), z.null()]),
|
||||
address_line_2: z.union([z.string(), z.null()]),
|
||||
address_line_3: z.union([z.string(), z.null()]),
|
||||
address_post_code: z.union([z.string(), z.null()]),
|
||||
address_city: z.union([z.string(), z.null()]),
|
||||
address_country: z.union([z.string(), z.null()]),
|
||||
phone: z.union([z.string(), z.null()]),
|
||||
email: z.union([z.string(), z.null()]),
|
||||
is_archived: z.boolean(),
|
||||
archived_at: z.union([z.string(), z.null()]),
|
||||
invoices_count: z.number().int(),
|
||||
has_non_draft_invoices: z.boolean(),
|
||||
created_at: z.union([z.string(), z.null()]),
|
||||
updated_at: z.union([z.string(), z.null()]),
|
||||
})
|
||||
.passthrough();
|
||||
const InvoiceRecipientCollection = z.array(InvoiceRecipientResource);
|
||||
const InvoiceRecipientRequest = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
vatin: z.union([z.string(), z.null()]).optional(),
|
||||
address_line_1: z.union([z.string(), z.null()]).optional(),
|
||||
address_line_2: z.union([z.string(), z.null()]).optional(),
|
||||
address_line_3: z.union([z.string(), z.null()]).optional(),
|
||||
address_post_code: z.union([z.string(), z.null()]).optional(),
|
||||
address_city: z.union([z.string(), z.null()]).optional(),
|
||||
address_country: z.union([z.string(), z.null()]).optional(),
|
||||
phone: z.union([z.string(), z.null()]).optional(),
|
||||
email: z.union([z.string(), z.null()]).optional(),
|
||||
is_archived: z.boolean().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
const InvoiceResource = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
organization_id: z.string(),
|
||||
invoice_recipient_id: z.string(),
|
||||
reference: z.string(),
|
||||
seller_name: z.string(),
|
||||
recipient: z.string(),
|
||||
buyer_name: z.string(),
|
||||
status: z.string(),
|
||||
status_label: z.string(),
|
||||
date: z.string(),
|
||||
due_at: z.string(),
|
||||
paid_date: z.string(),
|
||||
@@ -116,7 +76,16 @@ const InvoiceStoreRequest = z
|
||||
seller_address_country: z.union([z.string(), z.null()]).optional(),
|
||||
seller_phone: z.union([z.string(), z.null()]).optional(),
|
||||
seller_email: z.union([z.string(), z.null()]).optional(),
|
||||
invoice_recipient_id: z.string(),
|
||||
buyer_name: z.string(),
|
||||
buyer_vatin: z.union([z.string(), z.null()]).optional(),
|
||||
buyer_address_line_1: z.union([z.string(), z.null()]).optional(),
|
||||
buyer_address_line_2: z.union([z.string(), z.null()]).optional(),
|
||||
buyer_address_line_3: z.union([z.string(), z.null()]).optional(),
|
||||
buyer_address_post_code: z.union([z.string(), z.null()]).optional(),
|
||||
buyer_address_city: z.union([z.string(), z.null()]).optional(),
|
||||
buyer_address_country: z.union([z.string(), z.null()]).optional(),
|
||||
buyer_phone: z.union([z.string(), z.null()]).optional(),
|
||||
buyer_email: z.union([z.string(), z.null()]).optional(),
|
||||
date: z.string(),
|
||||
billing_period_start: z.union([z.string(), z.null()]).optional(),
|
||||
billing_period_end: z.union([z.string(), z.null()]).optional(),
|
||||
@@ -161,7 +130,6 @@ const DetailedInvoiceResource = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
organization_id: z.string(),
|
||||
invoice_recipient_id: z.string(),
|
||||
reference: z.string(),
|
||||
seller_name: z.string(),
|
||||
seller_vatin: z.string(),
|
||||
@@ -173,7 +141,16 @@ const DetailedInvoiceResource = z
|
||||
seller_address_country: z.string(),
|
||||
seller_phone: z.string(),
|
||||
seller_email: z.string(),
|
||||
recipient: InvoiceRecipientResource,
|
||||
buyer_name: z.string(),
|
||||
buyer_vatin: z.string(),
|
||||
buyer_address_line_1: z.string(),
|
||||
buyer_address_line_2: z.string(),
|
||||
buyer_address_line_3: z.string(),
|
||||
buyer_address_post_code: z.string(),
|
||||
buyer_address_city: z.string(),
|
||||
buyer_address_country: z.string(),
|
||||
buyer_phone: z.string(),
|
||||
buyer_email: z.string(),
|
||||
paid_date: z.string(),
|
||||
due_at: z.string(),
|
||||
discount_type: z.string(),
|
||||
@@ -194,7 +171,7 @@ const DetailedInvoiceResource = z
|
||||
entries: z.array(InvoiceEntryResource),
|
||||
})
|
||||
.passthrough();
|
||||
const InvoiceStatus = z.enum(['draft', 'sent', 'paid', 'cancelled']);
|
||||
const InvoiceStatus = z.enum(['draft', 'sent', 'cancelled']);
|
||||
const InvoiceUpdateRequest = z
|
||||
.object({
|
||||
status: InvoiceStatus,
|
||||
@@ -210,7 +187,16 @@ const InvoiceUpdateRequest = z
|
||||
seller_address_country: z.union([z.string(), z.null()]),
|
||||
seller_phone: z.union([z.string(), z.null()]),
|
||||
seller_email: z.union([z.string(), z.null()]),
|
||||
invoice_recipient_id: z.string(),
|
||||
buyer_name: z.string(),
|
||||
buyer_vatin: z.union([z.string(), z.null()]),
|
||||
buyer_address_line_1: z.union([z.string(), z.null()]),
|
||||
buyer_address_line_2: z.union([z.string(), z.null()]),
|
||||
buyer_address_line_3: z.union([z.string(), z.null()]),
|
||||
buyer_address_post_code: z.union([z.string(), z.null()]),
|
||||
buyer_address_city: z.union([z.string(), z.null()]),
|
||||
buyer_address_country: z.union([z.string(), z.null()]),
|
||||
buyer_phone: z.union([z.string(), z.null()]),
|
||||
buyer_email: z.union([z.string(), z.null()]),
|
||||
date: z.string(),
|
||||
billing_period_start: z.union([z.string(), z.null()]),
|
||||
billing_period_end: z.union([z.string(), z.null()]),
|
||||
@@ -1899,125 +1885,6 @@ const endpoints = makeApi([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: '/v1/organizations/:organization/invoice-recipients',
|
||||
alias: 'getInvoiceRecipients',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.object({ data: InvoiceRecipientCollection }).passthrough(),
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/v1/organizations/:organization/invoice-recipients',
|
||||
alias: 'createInvoiceRecipient',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: InvoiceRecipientRequest,
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient',
|
||||
alias: 'getInvoiceRecipient',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
{
|
||||
name: 'invoiceRecipient',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
|
||||
},
|
||||
{
|
||||
method: 'put',
|
||||
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient',
|
||||
alias: 'updateInvoiceRecipient',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: InvoiceRecipientRequest,
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
{
|
||||
name: 'invoiceRecipient',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient/duplicate',
|
||||
alias: 'duplicateInvoiceRecipient',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: InvoiceRecipientRequest,
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
{
|
||||
name: 'invoiceRecipient',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
|
||||
},
|
||||
{
|
||||
method: 'delete',
|
||||
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient',
|
||||
alias: 'deleteInvoiceRecipient',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
{
|
||||
name: 'invoiceRecipient',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: '/v1/organizations/:organization/invoices',
|
||||
@@ -2034,11 +1901,6 @@ const endpoints = makeApi([
|
||||
type: 'Query',
|
||||
schema: z.number().int().gte(1).lte(2147483647).optional(),
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'Query',
|
||||
schema: InvoiceStatus.optional(),
|
||||
},
|
||||
],
|
||||
response: z.object({ data: InvoiceCollection }).passthrough(),
|
||||
errors: [
|
||||
|
||||
@@ -92,30 +92,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||
$response->assertJsonPath('data.0.id', $timeEntry->getKey());
|
||||
}
|
||||
|
||||
public function test_index_endpoint_filters_by_member_id_instead_of_legacy_user_id(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'time-entries:view:own',
|
||||
]);
|
||||
$legacyUser = User::factory()->create();
|
||||
$timeEntry = TimeEntry::factory()->forMember($data->member)->create([
|
||||
'user_id' => $legacyUser->getKey(),
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||
$data->organization->getKey(),
|
||||
'member_id' => $data->member->getKey(),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
$this->assertResponseCode($response, 200);
|
||||
$response->assertJsonCount(1, 'data');
|
||||
$response->assertJsonPath('data.0.id', $timeEntry->getKey());
|
||||
}
|
||||
|
||||
public function test_index_endpoint_fails_if_user_filter_is_from_different_organization(): void
|
||||
{
|
||||
// Arrange
|
||||
@@ -150,10 +126,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||
$data->organization->getKey(),
|
||||
'member_id' => $member->getKey(),
|
||||
]));
|
||||
$response = $this->getJson(route('api.v1.time-entries.index', [$data->organization->getKey(), 'user_id' => $user->getKey()]));
|
||||
|
||||
// Assert
|
||||
$this->assertResponseCode($response, 200);
|
||||
@@ -1799,29 +1772,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_aggregate_endpoint_filters_by_member_id_instead_of_legacy_user_id(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'time-entries:view:own',
|
||||
]);
|
||||
$legacyUser = User::factory()->create();
|
||||
TimeEntry::factory()->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create([
|
||||
'user_id' => $legacyUser->getKey(),
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson(route('api.v1.time-entries.aggregate', [
|
||||
$data->organization->getKey(),
|
||||
'member_id' => $data->member->getKey(),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonPath('data.seconds', 100);
|
||||
}
|
||||
|
||||
public function test_aggregate_endpoint_groups_by_two_groups(): void
|
||||
{
|
||||
// Arrange
|
||||
@@ -2869,32 +2819,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_updates_user_id_when_member_id_changes(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'time-entries:update:all',
|
||||
]);
|
||||
$otherUser = User::factory()->create();
|
||||
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
|
||||
$timeEntry = TimeEntry::factory()->forMember($data->member)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
||||
'member_id' => $otherMember->getKey(),
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertValid();
|
||||
$this->assertResponseCode($response, 200);
|
||||
$this->assertDatabaseHas(TimeEntry::class, [
|
||||
'id' => $timeEntry->getKey(),
|
||||
'member_id' => $otherMember->getKey(),
|
||||
'user_id' => $otherUser->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_can_update_project_and_automatically_set_client(): void
|
||||
{
|
||||
// Arrange
|
||||
@@ -3231,40 +3155,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_destroy_multiple_uses_member_id_for_own_permission_checks(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'time-entries:delete:own',
|
||||
]);
|
||||
$otherUser = User::factory()->create();
|
||||
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
|
||||
$timeEntry = TimeEntry::factory()->forMember($otherMember)->create([
|
||||
'user_id' => $data->user->getKey(),
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.time-entries.destroy-multiple', [$data->organization->getKey()]), [
|
||||
'ids' => [
|
||||
$timeEntry->getKey(),
|
||||
],
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertValid();
|
||||
$this->assertResponseCode($response, 200);
|
||||
$response->assertExactJson([
|
||||
'success' => [],
|
||||
'error' => [
|
||||
$timeEntry->getKey(),
|
||||
],
|
||||
]);
|
||||
$this->assertDatabaseHas(TimeEntry::class, [
|
||||
'id' => $timeEntry->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_destroy_multiple_deletes_all_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_all_time_entries_permission(): void
|
||||
{
|
||||
// Arrange
|
||||
@@ -3676,46 +3566,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_multiple_uses_member_id_for_own_permission_checks(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'time-entries:update:own',
|
||||
'projects:view:all',
|
||||
]);
|
||||
$otherUser = User::factory()->create();
|
||||
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
|
||||
$timeEntry = TimeEntry::factory()->forMember($otherMember)->create([
|
||||
'user_id' => $data->user->getKey(),
|
||||
]);
|
||||
$timeEntriesFake = TimeEntry::factory()->forOrganization($data->organization)->make();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->patchJson(route('api.v1.time-entries.update-multiple', [$data->organization->getKey()]), [
|
||||
'ids' => [
|
||||
$timeEntry->getKey(),
|
||||
],
|
||||
'changes' => [
|
||||
'description' => $timeEntriesFake->description,
|
||||
],
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertValid();
|
||||
$this->assertResponseCode($response, 200);
|
||||
$response->assertExactJson([
|
||||
'success' => [],
|
||||
'error' => [
|
||||
$timeEntry->getKey(),
|
||||
],
|
||||
]);
|
||||
$this->assertDatabaseHas(TimeEntry::class, [
|
||||
'id' => $timeEntry->getKey(),
|
||||
'description' => $timeEntry->description,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_multiple_updates_sets_description_to_empty_if_the_client_sends_null(): void
|
||||
{
|
||||
// Arrange
|
||||
@@ -3762,51 +3612,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_multiple_updates_user_id_when_member_id_changes(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'time-entries:update:all',
|
||||
]);
|
||||
$otherUser = User::factory()->create();
|
||||
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
|
||||
$timeEntry1 = TimeEntry::factory()->forMember($data->member)->create();
|
||||
$timeEntry2 = TimeEntry::factory()->forMember($data->member)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->patchJson(route('api.v1.time-entries.update-multiple', [$data->organization->getKey()]), [
|
||||
'ids' => [
|
||||
$timeEntry1->getKey(),
|
||||
$timeEntry2->getKey(),
|
||||
],
|
||||
'changes' => [
|
||||
'member_id' => $otherMember->getKey(),
|
||||
],
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertValid();
|
||||
$response->assertStatus(200);
|
||||
$response->assertExactJson([
|
||||
'success' => [
|
||||
$timeEntry1->getKey(),
|
||||
$timeEntry2->getKey(),
|
||||
],
|
||||
'error' => [],
|
||||
]);
|
||||
$this->assertDatabaseHas(TimeEntry::class, [
|
||||
'id' => $timeEntry1->getKey(),
|
||||
'member_id' => $otherMember->getKey(),
|
||||
'user_id' => $otherUser->getKey(),
|
||||
]);
|
||||
$this->assertDatabaseHas(TimeEntry::class, [
|
||||
'id' => $timeEntry2->getKey(),
|
||||
'member_id' => $otherMember->getKey(),
|
||||
'user_id' => $otherUser->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_multiple_updates_all_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_all_time_entries_permission(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Reference in New Issue
Block a user