From cda20beb9e5e5d5c48758c871dd06387d55dca67 Mon Sep 17 00:00:00 2001 From: Gunadi Wirawan Date: Sun, 23 Oct 2022 18:34:45 +0800 Subject: [PATCH 01/21] sales return: update email template --- .../return-approval-request-single.blade.php | 222 +++++++++--------- .../return/return-approval-request.blade.php | 221 +++++++++-------- 2 files changed, 221 insertions(+), 222 deletions(-) diff --git a/resources/views/emails/sales/return/return-approval-request-single.blade.php b/resources/views/emails/sales/return/return-approval-request-single.blade.php index 846673e76..7a960ea4e 100644 --- a/resources/views/emails/sales/return/return-approval-request-single.blade.php +++ b/resources/views/emails/sales/return/return-approval-request-single.blade.php @@ -6,51 +6,35 @@ 'approver_id' => $approver->id, 'token' => $approver->token ]; - $urlApprovalQueries = array_merge($urlQueries, ['resource-type' => 'salesReturns']); + $urlApprovalQueries = array_merge($urlQueries, ['resource-type' => 'SalesReturn']); @endphp -
{{ $salesReturns[0]->form->cancellation_status ? 'Cancellation' : '' }} Approval Email
+
Request Approval All

Hello Mrs/Mr/Ms {{ $approver->getFullNameAttribute() }},
- You Have {{ $salesReturns[0]->form->cancellation_status ? 'a cancellation' : 'an' }} approval for Sales Return. we would like to details as follows: + You Have an approval for Sales Return. we would like to details as follows:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Form Number: {{ $salesReturns[0]->form->number ?: '-' }}
Form Date: {{ date('d F Y', strtotime($salesReturns[0]->form->date)) ?: '-' }}
Form Reference: {{ $salesReturns[0]->salesInvoice->form->number ?: '-' }}
Customer: {{ $salesReturns[0]->customer->name ?: '-' }}
Create at: {{ date('d F Y', strtotime($salesReturns[0]->form->date)) ?: '-' }}
Create by: {{ $salesReturns[0]->form->createdBy->getFullNameAttribute() ?: '-' }}
Notes: {{ $salesReturns[0]->form->notes ?: '-' }}
+ + + + + + + + + + + + + + + +
Form Number: {{ optional($form)->number }}
Form Date: {{ optional($form)->date }}
Create at: {{ optional($form)->created }}
- - - - - - + + + + + + + + + - @foreach($salesReturns[0]->items as $item) + @foreach($salesReturns as $salesReturn) + @php + $salesReturnForm = $salesReturn->form; + $urlApprovalQueries['ids'] = $salesReturn->id; + $urlApprovalQueries['crud-type'] = $salesReturn->action; + @endphp - - + + + - - - @endforeach - - - - - - - - - - - - - - - -
NoItem NameQuantity SalesQuantity ReturnPriceDiscountTotalForm DateForm NumberForm ReferenceCustomer + + + + + + +
ItemQuantity Return
+
NoteCreated ByCreated At
{{ $loop->iteration }} - {{ $item->item->name }} + {{ date('d M Y', strtotime($salesReturnForm->date)) }} - {{ $item->quantity_sales }} + + {{ $salesReturnForm->number }} + {{ ' ' }} + {{ + !is_null($salesReturnForm->close_status) + && in_array($salesReturnForm->close_status, [0, 1]) + ? ' - Closed' + : '' + }} - {{ $item->quantity }} + + {{ $salesReturn->salesInvoice->form->number }} + + {{ $salesReturn->customer->name }} + + + + @foreach($salesReturn->items as $item) + @php $borderBottom = !$loop->last ? 'border-bottom: 1px solid black' : ''; @endphp + + + + + + @endforeach + +
+ {{ $item->item->name }} + + {{ $item->quantity }} +
+
+ {{ $item->note }} - {{ number_format($item->price) }} + + {{ $salesReturnForm->createdBy->getFullNameAttribute() }} - {{ number_format($item->discount_value) }} + + {{ date('d M Y, H:i', strtotime($salesReturnForm->created_at)) }} - {{ number_format($item->quantity * ($item->price - $item->discount_value)) }} + +
- Sub Total - - {{ number_format($salesReturns[0]->amount - $salesReturns[0]->tax) }} -
- Taxbase - - {{ number_format($salesReturns[0]->amount - $salesReturns[0]->tax) }} -
- Tax - - {{ number_format($salesReturns[0]->tax) }} -
- Taxbase - - {{ number_format($salesReturns[0]->amount) }} -
- @if (@$url)
+ @php + unset($urlApprovalQueries['crud-type']); + $urlApprovalQueries['ids'] = implode(",", Illuminate\Support\Arr::pluck($salesReturns, 'id')); + @endphp - Check - - - Approve + Approve All - Reject + style="background-color: rgb(238, 238, 238); border: none; color: rgb(83, 83, 83); margin:8px 0; padding: 8px 16px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; "> + Reject All
- @else -

- Open your dashboard to check. -

- @endif
-@stop +@stop \ No newline at end of file diff --git a/resources/views/emails/sales/return/return-approval-request.blade.php b/resources/views/emails/sales/return/return-approval-request.blade.php index 862e811c3..993227362 100644 --- a/resources/views/emails/sales/return/return-approval-request.blade.php +++ b/resources/views/emails/sales/return/return-approval-request.blade.php @@ -6,35 +6,51 @@ 'approver_id' => $approver->id, 'token' => $approver->token ]; - $urlApprovalQueries = array_merge($urlQueries, ['resource-type' => 'SalesReturn']); + $urlApprovalQueries = array_merge($urlQueries, ['resource-type' => 'salesReturns']); @endphp -
Request Approval All
+
{{ $salesReturns[0]->form->cancellation_status ? 'Cancellation' : '' }} Approval Email

Hello Mrs/Mr/Ms {{ $approver->getFullNameAttribute() }},
- You Have an approval for Sales Return. we would like to details as follows: + You Have {{ $salesReturns[0]->form->cancellation_status ? 'a cancellation' : 'an' }} approval for Sales Return. we would like to details as follows:
- - - - - - - - - - - - - - - -
Form Number: {{ optional($form)->number }}
Form Date: {{ optional($form)->date }}
Create at: {{ optional($form)->created }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Form Number: {{ $salesReturns[0]->form->number ?: '-' }}
Form Date: {{ date('d F Y', strtotime($salesReturns[0]->form->date)) ?: '-' }}
Form Reference: {{ $salesReturns[0]->salesInvoice->form->number ?: '-' }}
Customer: {{ $salesReturns[0]->customer->name ?: '-' }}
Create at: {{ date('d F Y', strtotime($salesReturns[0]->form->date)) ?: '-' }}
Create by: {{ $salesReturns[0]->form->createdBy->getFullNameAttribute() ?: '-' }}
Notes: {{ $salesReturns[0]->form->notes ?: '-' }}
- - - - - - - - - + + + + + + - @foreach($salesReturns as $salesReturn) - @php - $salesReturnForm = $salesReturn->form; - - $urlApprovalQueries['ids'] = $salesReturn->id; - $urlApprovalQueries['crud-type'] = $salesReturn->action; - @endphp + @foreach($salesReturns[0]->items as $item) - - - - - - - - @endforeach + + + + + + + + + + + + + + + +
NoForm DateForm NumberForm ReferenceCustomer - - - - - - -
ItemQuantity Return
-
NoteCreated ByCreated AtItem NameQuantity SalesQuantity ReturnPriceDiscountTotal
{{ $loop->iteration }} - {{ date('d M Y', strtotime($salesReturnForm->date)) }} + {{ $item->item->name }} - {{ $salesReturnForm->number }} - {{ ' ' }} - {{ - !is_null($salesReturnForm->close_status) - && in_array($salesReturnForm->close_status, [0, 1]) - ? ' - Closed' - : '' - }} + + {{ $item->quantity_sales }} - {{ $salesReturn->salesInvoice->form->number }} + + {{ $item->quantity }} - {{ $salesReturn->customer->name }} + + {{ number_format($item->price) }} - - - @foreach($salesReturn->items as $item) - @php $borderBottom = !$loop->last ? 'border-bottom: 1px solid black' : ''; @endphp - - - - - - @endforeach - -
- {{ $item->item->name }} - - {{ $item->quantity }} -
+
+ {{ number_format($item->discount_value) }} - {{ $item->note }} - - {{ $salesReturnForm->createdBy->getFullNameAttribute() }} - - {{ date('d M Y, H:i', strtotime($salesReturnForm->created_at)) }} - - + + {{ number_format($item->quantity * ($item->price - $item->discount_value)) }}
+ Sub Total + + {{ number_format($salesReturns[0]->amount - $salesReturns[0]->tax) }} +
+ Taxbase + + {{ number_format($salesReturns[0]->amount - $salesReturns[0]->tax) }} +
+ Tax + + {{ number_format($salesReturns[0]->tax) }} +
+ Taxbase + + {{ number_format($salesReturns[0]->amount) }} +
+ @if (@$url)
- @php - unset($urlApprovalQueries['crud-type']); - $urlApprovalQueries['ids'] = implode(",", Illuminate\Support\Arr::pluck($salesReturns, 'id')); - @endphp + Check + + - Approve All + Approve - Reject All + style="background-color: rgb(255, 0, 0); border: none; color: white; margin:8px 0; padding: 8px 16px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; "> + Reject
+ @else +

+ Open your dashboard to check. +

+ @endif
@stop From a64fc30fb2b196a1b5e8123093dafe77772e9f0c Mon Sep 17 00:00:00 2001 From: Gunadi Wirawan Date: Sun, 23 Oct 2022 18:42:16 +0800 Subject: [PATCH 02/21] sales return: update email template --- .../return-approval-request-single.blade.php | 222 +++++++++-------- .../return/return-approval-request.blade.php | 224 ++++++++++-------- 2 files changed, 225 insertions(+), 221 deletions(-) diff --git a/resources/views/emails/sales/return/return-approval-request-single.blade.php b/resources/views/emails/sales/return/return-approval-request-single.blade.php index 7a960ea4e..993227362 100644 --- a/resources/views/emails/sales/return/return-approval-request-single.blade.php +++ b/resources/views/emails/sales/return/return-approval-request-single.blade.php @@ -6,35 +6,51 @@ 'approver_id' => $approver->id, 'token' => $approver->token ]; - $urlApprovalQueries = array_merge($urlQueries, ['resource-type' => 'SalesReturn']); + $urlApprovalQueries = array_merge($urlQueries, ['resource-type' => 'salesReturns']); @endphp -
Request Approval All
+
{{ $salesReturns[0]->form->cancellation_status ? 'Cancellation' : '' }} Approval Email

Hello Mrs/Mr/Ms {{ $approver->getFullNameAttribute() }},
- You Have an approval for Sales Return. we would like to details as follows: + You Have {{ $salesReturns[0]->form->cancellation_status ? 'a cancellation' : 'an' }} approval for Sales Return. we would like to details as follows:
- - - - - - - - - - - - - - - -
Form Number: {{ optional($form)->number }}
Form Date: {{ optional($form)->date }}
Create at: {{ optional($form)->created }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Form Number: {{ $salesReturns[0]->form->number ?: '-' }}
Form Date: {{ date('d F Y', strtotime($salesReturns[0]->form->date)) ?: '-' }}
Form Reference: {{ $salesReturns[0]->salesInvoice->form->number ?: '-' }}
Customer: {{ $salesReturns[0]->customer->name ?: '-' }}
Create at: {{ date('d F Y', strtotime($salesReturns[0]->form->date)) ?: '-' }}
Create by: {{ $salesReturns[0]->form->createdBy->getFullNameAttribute() ?: '-' }}
Notes: {{ $salesReturns[0]->form->notes ?: '-' }}
- - - - - - - - - + + + + + + - @foreach($salesReturns as $salesReturn) - @php - $salesReturnForm = $salesReturn->form; - $urlApprovalQueries['ids'] = $salesReturn->id; - $urlApprovalQueries['crud-type'] = $salesReturn->action; - @endphp + @foreach($salesReturns[0]->items as $item) - - - - - - - - @endforeach + + + + + + + + + + + + + + + +
NoForm DateForm NumberForm ReferenceCustomer - - - - - - -
ItemQuantity Return
-
NoteCreated ByCreated AtItem NameQuantity SalesQuantity ReturnPriceDiscountTotal
{{ $loop->iteration }} - {{ date('d M Y', strtotime($salesReturnForm->date)) }} + {{ $item->item->name }} - {{ $salesReturnForm->number }} - {{ ' ' }} - {{ - !is_null($salesReturnForm->close_status) - && in_array($salesReturnForm->close_status, [0, 1]) - ? ' - Closed' - : '' - }} + + {{ $item->quantity_sales }} - {{ $salesReturn->salesInvoice->form->number }} - - {{ $salesReturn->customer->name }} - - - - @foreach($salesReturn->items as $item) - @php $borderBottom = !$loop->last ? 'border-bottom: 1px solid black' : ''; @endphp - - - - - - @endforeach - -
- {{ $item->item->name }} - - {{ $item->quantity }} -
-
- {{ $item->note }} + + {{ $item->quantity }} - {{ $salesReturnForm->createdBy->getFullNameAttribute() }} + + {{ number_format($item->price) }} - {{ date('d M Y, H:i', strtotime($salesReturnForm->created_at)) }} + + {{ number_format($item->discount_value) }} - + + {{ number_format($item->quantity * ($item->price - $item->discount_value)) }}
+ Sub Total + + {{ number_format($salesReturns[0]->amount - $salesReturns[0]->tax) }} +
+ Taxbase + + {{ number_format($salesReturns[0]->amount - $salesReturns[0]->tax) }} +
+ Tax + + {{ number_format($salesReturns[0]->tax) }} +
+ Taxbase + + {{ number_format($salesReturns[0]->amount) }} +
+ @if (@$url)
- @php - unset($urlApprovalQueries['crud-type']); - $urlApprovalQueries['ids'] = implode(",", Illuminate\Support\Arr::pluck($salesReturns, 'id')); - @endphp + Check + + - Approve All + Approve - Reject All + style="background-color: rgb(255, 0, 0); border: none; color: white; margin:8px 0; padding: 8px 16px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; "> + Reject
+ @else +

+ Open your dashboard to check. +

+ @endif
-@stop \ No newline at end of file +@stop diff --git a/resources/views/emails/sales/return/return-approval-request.blade.php b/resources/views/emails/sales/return/return-approval-request.blade.php index 993227362..5e427af0f 100644 --- a/resources/views/emails/sales/return/return-approval-request.blade.php +++ b/resources/views/emails/sales/return/return-approval-request.blade.php @@ -6,51 +6,35 @@ 'approver_id' => $approver->id, 'token' => $approver->token ]; - $urlApprovalQueries = array_merge($urlQueries, ['resource-type' => 'salesReturns']); + $urlApprovalQueries = array_merge($urlQueries, ['resource-type' => 'SalesReturn']); @endphp -
{{ $salesReturns[0]->form->cancellation_status ? 'Cancellation' : '' }} Approval Email
+
Request Approval All

Hello Mrs/Mr/Ms {{ $approver->getFullNameAttribute() }},
- You Have {{ $salesReturns[0]->form->cancellation_status ? 'a cancellation' : 'an' }} approval for Sales Return. we would like to details as follows: + You Have an approval for Sales Return. we would like to details as follows:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Form Number: {{ $salesReturns[0]->form->number ?: '-' }}
Form Date: {{ date('d F Y', strtotime($salesReturns[0]->form->date)) ?: '-' }}
Form Reference: {{ $salesReturns[0]->salesInvoice->form->number ?: '-' }}
Customer: {{ $salesReturns[0]->customer->name ?: '-' }}
Create at: {{ date('d F Y', strtotime($salesReturns[0]->form->date)) ?: '-' }}
Create by: {{ $salesReturns[0]->form->createdBy->getFullNameAttribute() ?: '-' }}
Notes: {{ $salesReturns[0]->form->notes ?: '-' }}
+ + + + + + + + + + + + + + + +
Form Number: {{ optional($form)->number }}
Form Date: {{ optional($form)->date }}
Create at: {{ optional($form)->created }}
- - + + + + + - - - + + + + - @foreach($salesReturns[0]->items as $item) + @foreach($salesReturns as $salesReturn) + @php + $salesReturnForm = $salesReturn->form; + + $urlApprovalQueries['ids'] = $salesReturn->id; + $urlApprovalQueries['crud-type'] = $salesReturn->action; + @endphp - - - - + + @foreach($salesReturn->items as $item) + + - + + + - + @php + ($first = true); + @endphp + @foreach($salesReturn->items as $item) + @if($first) + @php + ($first = false); + @endphp + @continue + @endif + + - + @endforeach @endforeach - - - - - - - - - - - - - - - -
NoItem NameQuantity SalesForm DateForm NumberForm ReferenceCustomerItem Quantity ReturnPriceDiscountTotalNoteCreated ByCreated At
+ {{ $loop->iteration }} - {{ $item->item->name }} + + {{ date('d M Y', strtotime($salesReturnForm->date)) }} - {{ $item->quantity_sales }} + + {{ $salesReturnForm->number }} + {{ ' ' }} + {{ + !is_null($salesReturnForm->close_status) + && in_array($salesReturnForm->close_status, [0, 1]) + ? ' - Closed' + : '' + }} + + {{ $salesReturn->salesInvoice->form->number }} + + {{ $salesReturn->customer->name }} + + {{ $item->item->name }} + {{ $item->quantity }} - {{ number_format($item->price) }} + @break + @endforeach + + {{ $salesReturnForm->notes }} + + {{ $salesReturnForm->createdBy->getFullNameAttribute() }} + + {{ date('d M Y, H:i', strtotime($salesReturnForm->created_at)) }} + + - {{ number_format($item->discount_value) }} +
+ {{ $item->item->name }} - {{ number_format($item->quantity * ($item->price - $item->discount_value)) }} + + {{ $item->quantity }}
- Sub Total - - {{ number_format($salesReturns[0]->amount - $salesReturns[0]->tax) }} -
- Taxbase - - {{ number_format($salesReturns[0]->amount - $salesReturns[0]->tax) }} -
- Tax - - {{ number_format($salesReturns[0]->tax) }} -
- Taxbase - - {{ number_format($salesReturns[0]->amount) }} -
- @if (@$url)
+ @php + unset($urlApprovalQueries['crud-type']); + $urlApprovalQueries['ids'] = implode(",", Illuminate\Support\Arr::pluck($salesReturns, 'id')); + @endphp - Check - - - Approve + Approve All - Reject + style="background-color: rgb(238, 238, 238); border: none; color: rgb(83, 83, 83); margin:8px 0; padding: 8px 16px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; "> + Reject All
- @else -

- Open your dashboard to check. -

- @endif
@stop From f18783488bee15a2a3732c46a9f8c374ca351007 Mon Sep 17 00:00:00 2001 From: "@gunadi" Date: Fri, 11 Nov 2022 12:27:52 +0800 Subject: [PATCH 03/21] middleware: update check default branch --- app/Http/Middleware/TenantModuleAccessMiddleware.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Middleware/TenantModuleAccessMiddleware.php b/app/Http/Middleware/TenantModuleAccessMiddleware.php index a7c2edb8e..fe909b2c9 100644 --- a/app/Http/Middleware/TenantModuleAccessMiddleware.php +++ b/app/Http/Middleware/TenantModuleAccessMiddleware.php @@ -49,12 +49,12 @@ public function handle($request, Closure $next, $module) throw new UnauthorizedException(); } + $this->_hasDefaultBranch(); + if ($this->action === 'read') { return $next($request); } - $this->_hasDefaultBranch(); - $this->_hasDefaultWarehouse(); $this->_isWarehouseBranchAsDefault(); From 97d9625e307f22a45f8ed0545e8312ad477ae0b2 Mon Sep 17 00:00:00 2001 From: Bayu Ramadhan Date: Sun, 13 Nov 2022 19:37:59 +0700 Subject: [PATCH 04/21] docker compose add phpmyadmin --- docker-compose.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 8490e6a15..3b08878fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,17 @@ services: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_DATABASE} + networks: + - point_network + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: 'phpmyadmin' + restart: unless-stopped + links: + - db + ports: + - '80:80' networks: - point_network networks: From abcba8a29a4107496d36498b4cd697edcc443bbb Mon Sep 17 00:00:00 2001 From: Bayu Ramadhan Date: Sun, 13 Nov 2022 19:38:20 +0700 Subject: [PATCH 05/21] fix init project --- app/Console/Commands/NewCommand.php | 4 ++++ composer.json | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Console/Commands/NewCommand.php b/app/Console/Commands/NewCommand.php index a4d221a58..853723cd5 100644 --- a/app/Console/Commands/NewCommand.php +++ b/app/Console/Commands/NewCommand.php @@ -46,6 +46,9 @@ public function handle() return; } + ini_set('memory_limit', '4095M'); + ini_set('max_execution_time', '0'); + $dbName = $this->argument('database_name') ?? env('DB_DATABASE'); $this->line('create '.$dbName.' database'); @@ -100,5 +103,6 @@ public function handle() $projectUser->save(); Artisan::call('tenant:database:reset', ['project_code' => 'dev']); + $this->line('process completed'); } } diff --git a/composer.json b/composer.json index 8eca6740e..3500a6b58 100644 --- a/composer.json +++ b/composer.json @@ -77,6 +77,7 @@ "config": { "preferred-install": "dist", "sort-packages": true, - "optimize-autoloader": true + "optimize-autoloader": true, + "process-timeout":0 } } From 1a6ec67d2a18429575823b3807f68bf8186b584a Mon Sep 17 00:00:00 2001 From: Bayu Ramadhan Date: Wed, 23 Nov 2022 22:38:39 +0700 Subject: [PATCH 06/21] purchase request - test case --- routes/api/purchase.php | 5 + .../Request/PurchaseRequestApprovalTest.php | 144 +++++++++++++ .../Request/PurchaseRequestCloseTest.php | 148 +++++++++++++ .../Purchase/Request/PurchaseRequestSetup.php | 112 ++++++++++ .../Purchase/Request/PurchaseRequestTest.php | 202 ++++++++++++++++++ tests/TestCase.php | 13 ++ 6 files changed, 624 insertions(+) create mode 100644 tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php create mode 100644 tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php create mode 100644 tests/Feature/Http/Purchase/Request/PurchaseRequestSetup.php create mode 100644 tests/Feature/Http/Purchase/Request/PurchaseRequestTest.php diff --git a/routes/api/purchase.php b/routes/api/purchase.php index 800ab4b31..b6561b5e8 100644 --- a/routes/api/purchase.php +++ b/routes/api/purchase.php @@ -10,6 +10,11 @@ Route::post('requests/{id}/reject', 'PurchaseRequest\\PurchaseRequestApprovalController@reject'); Route::post('requests/{id}/cancellation-approve', 'PurchaseRequest\\PurchaseRequestCancellationApprovalController@approve'); Route::post('requests/{id}/cancellation-reject', 'PurchaseRequest\\PurchaseRequestCancellationApprovalController@reject'); + Route::post('requests/{id}/close', 'PurchaseRequest\\PurchaseRequestCloseController@close'); + Route::post('requests/{id}/close-approve', 'PurchaseRequest\\PurchaseRequestCloseController@approve'); + Route::post('requests/{id}/close-reject', 'PurchaseRequest\\PurchaseRequestCloseController@reject'); + Route::post('requests/send-bulk-request-approval', 'PurchaseRequest\\PurchaseRequestController@sendBulkRequestApproval'); + Route::post('requests/approval-with-token/bulk', 'PurchaseRequest\\PurchaseRequestApprovalController@bulkApprovalWithToken'); Route::apiResource('requests', 'PurchaseRequest\\PurchaseRequestController'); Route::post('orders/{id}/approve', 'PurchaseOrder\\PurchaseOrderApprovalController@approve'); Route::post('orders/{id}/reject', 'PurchaseOrder\\PurchaseOrderApprovalController@reject'); diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php new file mode 100644 index 000000000..e98ca96e5 --- /dev/null +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php @@ -0,0 +1,144 @@ +createDataPurchaseRequest(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + // save data + $this->purchase = json_decode($response->getContent())->data; + } + + /** @test */ + public function unauthorized_reject_purchase_request() + { + $this->success_create_purchase_request(); + $this->unsetUserRole(); + + $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/reject', [], $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Unauthorized" + ]); + } + + /** @test */ + public function reject_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + /* s: reject test */ + $data = [ + 'id' => $this->purchase->id, + ]; + + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/reject', $data, $this->headers); + $response->assertStatus(200); + /* e: reject test */ + } + + /** @test */ + public function unauthorized_approve_purchase_request() + { + $this->success_create_purchase_request(); + $this->unsetUserRole(); + + $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/approve', [], $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Unauthorized" + ]); + } + + /** @test */ + public function approve_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + /* s: reject test */ + $data = [ + 'id' => $this->purchase->id + ]; + + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/approve', $data, $this->headers); + $response->assertStatus(200); + /* e: reject test */ + } + + /** @test */ + public function failed_request_approval_by_email_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $response = $this->json('POST', self::$path.'/send-bulk-request-approval', [], $this->headers); + $response->assertStatus(422); + } + + /** @test */ + public function success_request_approval_by_email_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + /* s: send request approval email */ + $data = [ + 'bulk_id'=> array($this->purchase->id), + 'tenant_url' => 'http://dev.localhost:8080' + ]; + + $response = $this->json('POST', self::$path.'/send-bulk-request-approval', $data, $this->headers); + $response->assertStatus(204); + /* e: send request approval email */ + } + + /** @test */ + public function failed_approval_by_email_purchase_request() + { + $this->success_request_approval_by_email_purchase_request(); + + /* s: bulk approval email fail test */ + $data = [ + 'token' => 'NGAWUR', + 'bulk_id' => array($this->purchase->id), + 'status' => -1 + ]; + + $response = $this->json('POST', self::$path.'/approval-with-token/bulk', $data, $this->headers); + $response->assertStatus(422); + /* e: bulk approval email fail test */ + } + + /** @test */ + public function success_approval_by_email_purchase_request() + { + $this->success_request_approval_by_email_purchase_request(); + + /* s: bulk approval email test */ + $token = Token::take(1)->first(); + $data = [ + 'token' => $token->token, + 'bulk_id' => array($this->purchase->id), + 'status' => -1 + ]; + + $response = $this->json('POST', self::$path.'/approval-with-token/bulk', $data, $this->headers); + $response->assertStatus(200); + /* e: bulk approval email test */ + } + +} \ No newline at end of file diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php new file mode 100644 index 000000000..f67cda0cb --- /dev/null +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php @@ -0,0 +1,148 @@ +createDataPurchaseRequest(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + // save data + $this->purchase = json_decode($response->getContent())->data; + } + + public function approve_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + $data = [ + 'id' => $this->purchase->id + ]; + + $this->json('POST', self::$path.'/'.$this->purchase->id.'/approve', $data, $this->headers); + } + + /** @test */ + public function invalid_state_close_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $data = [ + "id" => $this->purchase->id, + ]; + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/close', $data, $this->headers); + $response->assertStatus(500); + } + + /** @test */ + public function invalid_condition_close_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $data = [ + "id" => $this->purchase->id, + "reason" => "sample reason" + ]; + + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/close', $data, $this->headers); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "form not approved and not in pending state" + ]); + } + + /** @test */ + public function success_close_purchase_request() + { + $this->approve_purchase_request(); + + $data = [ + "id" => $this->purchase->id, + "reason" => "sample reason" + ]; + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/close', $data, $this->headers); + $response->assertStatus(204); + } + + /** @test */ + public function invalid_state_close_approve_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-approve', [], $this->headers); + $response->assertStatus(422); + } + + /** @test */ + public function success_close_approve_purchase_request() + { + $this->success_close_purchase_request(); + + $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-approve', [], $this->headers); + $response->assertStatus(200); + } + + /** @test */ + public function invalid_close_reject_purchase_request() + { + $this->success_close_purchase_request(); + + $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-reject', [], $this->headers); + $response->assertStatus(422); + } + + /** @test */ + public function invalid_state_close_reject_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $data["reason"] = "reject"; + $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-approve', $data, $this->headers); + $response->assertStatus(422); + } + + /** @test */ + public function success_reject_purchase_request() + { + $this->success_close_purchase_request(); + + $data['reason'] = $this->faker->text(200); + $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-reject', $data, $this->headers); + + $response->assertStatus(200); + } + + /** @test */ + public function success_autoclose_purchase_request() + { + // $this->expectOutputString(''); + $data = $this->createDataPurchaseRequest(); + foreach($data['items'] as $key=>$item){ + $data['items'][$key]['quantity_remaining'] = 0; + } + + $response = $this->json('POST', self::$path, $data, $this->headers); + $response->assertStatus(201); + + // save data + $this->purchase = json_decode($response->getContent())->data; + + $this->assertDatabaseHas('forms', [ + 'number' => $this->purchase->form->number, + 'close_status' => true, + 'close_approval_reason' => 'Closed by system' + ], 'tenant'); + } +} diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestSetup.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestSetup.php new file mode 100644 index 000000000..47087bb65 --- /dev/null +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestSetup.php @@ -0,0 +1,112 @@ +signIn(); + $this->setProject(); + $this->setPurchaseRequestPermission(); + $this->createSampleChartAccountType(); + $this->createSampleEmployee(); + $this->createSampleItem(); + $this->createSampleAllocation(); + } + + protected function setPurchaseRequestPermission() + { + $this->setRole(); + Permission::createIfNotExists('menu purchase'); + + $permission = ['purchase request']; + + foreach ($permission as $permission) { + Permission::createIfNotExists('create '.$permission); + Permission::createIfNotExists('read '.$permission); + Permission::createIfNotExists('update '.$permission); + Permission::createIfNotExists('delete '.$permission); + Permission::createIfNotExists('approve '.$permission); + } + + $permissions = Permission::all(); + $this->role->syncPermissions($permissions); + } + + protected function createSampleItem() + { + $item = new Item; + $item->code = "Code001"; + $item->name = "Kopi Jowo"; + $item->chart_of_account_id = $this->account->id; + $item->require_expiry_date = false; + $item->require_production_number = false; + $item->save(); + $this->item = $item; + } + + protected function createSampleAllocation() + { + $allocation = new Allocation; + $allocation->name = "Stok Pantry"; + $allocation->save(); + $this->allocation = $allocation; + } + + private function createDataPurchaseRequest() + { + $data = [ + "increment_group" => date('Ym'), + "date" => date('Y-m-d H:m:s'), + "required_date" => date('Y-m-d H:m:s'), + 'employee_id' => $this->employee->id, + "request_approval_to" => $this->user->id, + "notes" => "Test Note", + "items" => [ + [ + "item_id" => $this->item->id, + "item_name" => $this->item->name, + "unit" => "PCS", + "converter" => "1.00", + "quantity" => "20", + "quantity_remaining" => "20", + "notes" => "notes", + "allocation_id" => $this->allocation->id, + ] + ] + ]; + return $data; + } + + protected function unsetUserRole() + { + ModelHasRole::where('role_id', $this->role->id) + ->where('model_type', 'App\Model\Master\User') + ->where('model_id', $this->user->id) + ->delete(); + } + + protected function setDefaultBranch($state = true) + { + $tenantUser = TenantUser::find($this->user->id); + foreach ($tenantUser->branches as $branch) { + $branch->pivot->is_default = $state; + $branch->pivot->save(); + } + } +} \ No newline at end of file diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestTest.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestTest.php new file mode 100644 index 000000000..423311578 --- /dev/null +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestTest.php @@ -0,0 +1,202 @@ + date('Ym'), + "date" => date('Y-m-d H:m:s'), + "required_date" => date('Y-m-d H:m:s'), + "notes" => "Test Note", + "items" => [] + ]; + + // $data = $this->createDataPurchaseRequest(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + // $response->dump(); + $response->assertStatus(422); + } + + /** @test */ + public function success_create_purchase_request() + { + $data = $this->createDataPurchaseRequest(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + // save data + $this->purchase = json_decode($response->getContent())->data; + + // assert status + $response->assertStatus(201); + // assert database + $this->assertDatabaseHas('purchase_requests', [ + 'id' => $this->purchase->id, + 'required_date' => date('Y-m-d H:m:s', strtotime($this->purchase->required_date.' -7 hour')) + ], 'tenant'); + $this->assertDatabaseHas('purchase_request_items', [ + 'id' => $this->purchase->items[0]->id, + 'purchase_request_id' => $this->purchase->id, + 'item_id' => $data['items'][0]['item_id'], + 'item_name' => $data['items'][0]['item_name'], + 'quantity' => $data['items'][0]['quantity'], + 'quantity_remaining' => $data['items'][0]['quantity_remaining'], + 'unit' => $data['items'][0]['unit'], + 'converter' => $data['items'][0]['converter'], + 'allocation_id' => $data['items'][0]['allocation_id'], + 'notes' => $data['items'][0]['notes'], + ], 'tenant'); + } + + /** @test */ + public function read_all_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $response = $this->json('GET', self::$path.'?join=form,items,item&fields=purchase_request.*&sort_by=-form.number&group_by=form.id&filter_form=notArchived%3Bnull&filter_like=%7B%7D&filter_not_null=form.number&%7B%22form.date%22:%22'.date('Y-m-01').'+00:00:00%22%7D&filter_date_max=%7B%22form.date%22:%22'.date('Y-m-d').'+23:59:59%22%7D&limit=10&includes=form%3Bitems.item&page=1', array(), $this->headers); + $response->assertStatus(200); + } + + /** @test */ + public function read_single_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $response = $this->json('GET', self::$path.'/'.$this->purchase->id.'?includes=items.item;items.allocation;form.requestApprovalTo;form.branch', array(), $this->headers); + $response->assertStatus(200); + } + + /** @test */ + public function failed_update_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $data = [ + "increment_group" => date('Ym'), + "date" => date('Y-m-d H:m:s'), + "required_date" => date('Y-m-d H:m:s'), + "notes" => "Test Note", + "items" => [] + ]; + + // $data = $this->createDataPurchaseRequest(); + + $response = $this->json('PATCH', self::$path.'/'.$this->purchase->id, $data, $this->headers); + // $response->dump(); + $response->assertStatus(422); + } + + /** @test */ + public function success_update_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + $data = $this->createDataPurchaseRequest(); + $data['id'] = $this->purchase->id; + $data['required_date'] = date('Y-m-30 H:m:s'); + + $response = $this->json('PATCH', self::$path.'/'.$this->purchase->id, $data, $this->headers); + $response->assertStatus(201); + + // save data + $this->purchase = json_decode($response->getContent())->data; + + $this->assertDatabaseHas('purchase_requests', [ + 'id' => $this->purchase->id, + 'required_date' => date('Y-m-d H:m:s', strtotime($data['required_date'].' -7 hour')) + ], 'tenant'); + $this->assertDatabaseHas('purchase_request_items', [ + 'id' => $this->purchase->items[0]->id, + 'purchase_request_id' => $this->purchase->id, + 'item_id' => $data['items'][0]['item_id'], + 'item_name' => $data['items'][0]['item_name'], + 'quantity' => $data['items'][0]['quantity'], + 'quantity_remaining' => $data['items'][0]['quantity_remaining'], + 'unit' => $data['items'][0]['unit'], + 'converter' => $data['items'][0]['converter'], + 'allocation_id' => $data['items'][0]['allocation_id'], + 'notes' => $data['items'][0]['notes'], + ], 'tenant'); + } + + /** @test */ + public function failed_delete_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, [], $this->headers); + $response->assertStatus(422); + } + + /** @test */ + public function failed_default_branch_delete_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + $this->setDefaultBranch(false); + + $data = [ + 'id' => $this->purchase->id, + 'reason' => 'Reason' + ]; + $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, $data, $this->headers); + $response->assertStatus(422); + } + + /** @test */ + public function success_delete_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + /* s: request cancellation test */ + $data = [ + 'id' => $this->purchase->id, + 'reason' => 'Reason' + ]; + $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, $data, $this->headers); + $response->assertStatus(204); + /* e: request cancellation test */ + } + + /** @test */ + public function success_approve_delete_purchase_request() + { + $this->success_delete_purchase_request(); + + /* s: cancellation approve test */ + $data = [ + 'id' => $this->purchase->id + ]; + + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/cancellation-approve', $data, $this->headers); + $response->assertStatus(200); + /* e: cancellation approve test */ + } + + /** @test */ + public function success_reject_delete_purchase_request() + { + $this->success_delete_purchase_request(); + + /* s: cancellation approve test */ + $data = [ + 'id' => $this->purchase->id + ]; + + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/cancellation-reject', $data, $this->headers); + $response->assertStatus(200); + /* e: cancellation approve test */ + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 3abc80670..959d6dc40 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,6 +4,7 @@ use App\Model\Accounting\ChartOfAccount; use App\Model\Accounting\ChartOfAccountType; +use App\Model\HumanResource\Employee\Employee; use App\Model\Master\Branch; use App\Model\Package; use App\Model\Project\Project; @@ -40,6 +41,8 @@ abstract class TestCase extends BaseTestCase protected $user; protected $account = null; + protected $employee = null; + protected $role = null; /** * Set up the test. @@ -143,6 +146,15 @@ protected function createSampleChartAccount($chartOfAccountType) $this->account = $chartOfAccount; } + protected function createSampleEmployee() + { + $employee = new Employee; + $employee->name = 'John Doe'; + $employee->personal_identity = 'PASSPORT 940001930211FA'; + $employee->save(); + $this->employee = $employee; + } + protected function setRole() { $role = \App\Model\Auth\Role::createIfNotExists('super admin'); @@ -151,6 +163,7 @@ protected function setRole() $hasRole->model_type = 'App\Model\Master\User'; $hasRole->model_id = $this->user->id; $hasRole->save(); + $this->role = $role; } protected function setPermission() From 4b9cceee9be3cc17f9ae099122d2e362395b06fc Mon Sep 17 00:00:00 2001 From: Christhofer Date: Fri, 2 Dec 2022 11:42:00 +0700 Subject: [PATCH 07/21] self host masbug flysystem gdrive adapter --- app/Services/Google/Drive.php | 2 +- app/Services/Google/GoogleDriveAdapter.php | 2148 ++++++++++++++++++++ app/Services/Google/StreamableUpload.php | 424 ++++ 3 files changed, 2573 insertions(+), 1 deletion(-) create mode 100644 app/Services/Google/GoogleDriveAdapter.php create mode 100644 app/Services/Google/StreamableUpload.php diff --git a/app/Services/Google/Drive.php b/app/Services/Google/Drive.php index 279d037e1..6fa1de8e4 100644 --- a/app/Services/Google/Drive.php +++ b/app/Services/Google/Drive.php @@ -16,7 +16,7 @@ public function __construct() // https://github.com/masbug/flysystem-google-drive-ext#using-with-laravel-framework $this->service = new \Google\Service\Drive($this->client); - $this->adapter = new \Masbug\Flysystem\GoogleDriveAdapter($this->service); + $this->adapter = new GoogleDriveAdapter($this->service); $this->driver = new \League\Flysystem\Filesystem($this->adapter); $this->disk = new \Illuminate\Filesystem\FilesystemAdapter($this->driver, $this->adapter); } diff --git a/app/Services/Google/GoogleDriveAdapter.php b/app/Services/Google/GoogleDriveAdapter.php new file mode 100644 index 000000000..63660396e --- /dev/null +++ b/app/Services/Google/GoogleDriveAdapter.php @@ -0,0 +1,2148 @@ + 'drive', + 'useHasDir' => false, + 'useDisplayPaths' => true, + 'usePermanentDelete' => false, + 'publishPermission' => [ + 'type' => 'anyone', + 'role' => 'reader', + 'withLink' => true + ], + 'appsExportMap' => [ + 'application/vnd.google-apps.document' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.google-apps.spreadsheet' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.google-apps.drawing' => 'application/pdf', + 'application/vnd.google-apps.presentation' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.google-apps.script' => 'application/vnd.google-apps.script+json', + 'default' => 'application/pdf' + ], + + 'parameters' => [], + + 'teamDriveId' => null, + + 'sanitize_chars' => [ + // sanitize filename + // file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words + // control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx + // non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN + // URI reserved https://tools.ietf.org/html/rfc3986#section-2.2 + // URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt + + // must not allow + '/', '\\', '?', '%', '*', ':', '|', '"', '<', '>', + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', + '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', + '\x7F', '\xA0', '\xAD', + + // optional + '#', '@', '!', '$', '&', '\'', '+', ';', '=', + '^', '~', '`', + ], + 'sanitize_replacement_char' => '_' + ]; + + /** + * A comma-separated list of spaces to query + * Supported values are 'drive', 'appDataFolder' and 'photos' + * + * @var string + */ + protected $spaces; + + /** + * Root path + * + * @var string + */ + protected $root; + + /** + * Permission array as published item + * + * @var array + */ + protected $publishPermission; + + /** + * Cache of file objects + * + * @var array + */ + private $cacheFileObjects = []; + + /** + * Cache of hasDir + * + * @var array + */ + private $cacheHasDirs = []; + + /** + * Use hasDir function + * + * @var bool + */ + private $useHasDir = false; + + /** + * Permanent delete files and directories, avoid setTrashed + * + * @var bool + */ + private $usePermanentDelete = false; + + /** + * Options array + * + * @var array + */ + private $options = []; + + /** + * Using display paths instead of virtual IDs + * + * @var bool + */ + private $useDisplayPaths = true; + + /** + * Resolved root ID + * + * @var string + */ + private $rootId = null; + + /** + * Full path => virtual ID cache + * + * @var array + */ + private $cachedPaths = []; + + /** + * Recent virtual ID => file object requests cache + * + * @var array + */ + private $requestedIds = []; + + /** + * @var array Optional parameters sent with each request (see Google_Service_Resource var stackParameters and https://developers.google.com/analytics/devguides/reporting/core/v4/parameters) + */ + private $optParams = []; + + /** + * GoogleDriveAdapter constructor. + * + * @param Drive $service + * @param string|null $root + * @param array $options + */ + public function __construct($service, $root = null, $options = []) + { + $this->service = $service; + + $this->options = array_replace_recursive(static::$defaultOptions, $options); + + $this->spaces = $this->options['spaces']; + $this->useHasDir = $this->options['useHasDir']; + $this->usePermanentDelete = $this->options['usePermanentDelete']; + $this->publishPermission = $this->options['publishPermission']; + $this->useDisplayPaths = $this->options['useDisplayPaths']; + $this->optParams = $this->cleanOptParameters($this->options['parameters']); + + if ($root !== null) { + $root = trim($root, '/'); + if ($root === '') { + $root = null; + } + } + + if (isset($this->options['teamDriveId'])) { + $this->root = null; + $this->setTeamDriveId($this->options['teamDriveId']); + if ($this->useDisplayPaths && $root !== null) { + // get real root id + $this->root = $this->toSingleVirtualPath($root, false, true, true, true); + + // reset cache + $this->rootId = $this->root; + $this->clearCache(); + } + } else { + if (!$this->useDisplayPaths || $root === null) { + if ($root === null) { + $root = 'root'; + } + $this->root = $root; + $this->setPathPrefix(''); + } else { + $this->root = 'root'; + $this->setPathPrefix(''); + + // get real root id + $this->root = $this->toSingleVirtualPath($root, false, true, true, true); + + // reset cache + $this->rootId = $this->root; + $this->clearCache(); + } + } + } + + /** + * Gets the service + * + * @return Google\Service\Drive + */ + public function getService() + { + $this->refreshToken(); + return $this->service; + } + + /** + * Allow to forcefully clear the cache to enable long running process + * + * @return void + */ + public function clearCache() + { + $this->cachedPaths = []; + $this->requestedIds = []; + $this->cacheFileObjects = []; + $this->cacheHasDirs = []; + } + + /** + * Allow to refresh tokens to enable long running process + * + * @return void + */ + public function refreshToken() + { + $client = $this->service->getClient(); + if ($client->isAccessTokenExpired()) { + if ($client->isUsingApplicationDefaultCredentials()) { + $client->fetchAccessTokenWithAssertion(); + } else { + $refreshToken = $client->getRefreshToken(); + if ($refreshToken) { + $client->fetchAccessTokenWithRefreshToken($refreshToken); + } + } + } + } + + protected function cleanOptParameters($parameters) + { + $operations = ['files.copy', 'files.create', 'files.delete', + 'files.trash', 'files.get', 'files.list', 'files.update', + 'files.watch']; + $clean = []; + + foreach ($operations as $operation) { + $clean[$operation] = []; + if (isset($parameters[$operation])) { + $clean[$operation] = $parameters[$operation]; + } + } + + foreach ($parameters as $key => $value) { + if (in_array($key, $operations)) { + unset($parameters[$key]); + } + } + + foreach ($operations as $operation) { + $clean[$operation] = array_merge_recursive($parameters, $clean[$operation]); + } + + return $clean; + } + + /** + * {@inheritdoc} + */ + public function write($path, $contents, Config $config) + { + $updating = null; + + if ($this->useDisplayPaths) { + try { + $virtual_path = $this->toVirtualPath($path, true, false); + $updating = true; // destination exists + } catch (FileNotFoundException $e) { + $updating = false; + [$parentDir, $fileName] = $this->splitPath($path, false); + $virtual_path = $this->toSingleVirtualPath($parentDir, false, true, true, true); + if ($virtual_path === '') { + $virtual_path = $fileName; + } else { + $virtual_path .= '/'.$fileName; + } + } + if ($updating && is_array($virtual_path)) { + // multiple destinations with the same display path -> remove all but the first created & the first gets replaced + if (count($virtual_path) > 1) { + // delete all but first + $this->delete_by_id( + array_map( + function ($p) { + return $this->splitPath($p, false)[1]; + }, + array_slice($virtual_path, 1) + ) + ); + } + $virtual_path = $virtual_path[0]; + } + } else { + $virtual_path = $path; + } + + return $this->upload($virtual_path, $contents, $config, $updating); + } + + /** + * {@inheritdoc} + */ + public function writeStream($path, $resource, Config $config) + { + return $this->write($path, $resource, $config); + } + + /** + * {@inheritdoc} + */ + public function update($path, $contents, Config $config) + { + return $this->write($path, $contents, $config); + } + + /** + * {@inheritdoc} + */ + public function updateStream($path, $resource, Config $config) + { + return $this->write($path, $resource, $config); + } + + /** + * {@inheritdoc} + */ + public function rename($path, $newpath) + { + $this->refreshToken(); + if ($this->useDisplayPaths) { + $path = $this->toVirtualPath($path, true, true); + $newpathDir = self::dirname($newpath); + try { + $toPath = $this->toVirtualPath($newpathDir, false, true); + } catch (FileNotFoundException $e) { + if ($this->createDir($newpathDir, new Config(), true) === false) { + return false; + } + $toPath = $this->toVirtualPath($newpathDir, false, true); + } + if ($toPath === '') { + $toPath = $this->root; + } + + [$oldParent, $fileId] = $this->splitPath($path); + $newParent = $toPath; + $newName = basename($newpath); + } else { + [$oldParent, $fileId] = $this->splitPath($path); + [$newParent, $newName] = $this->splitPath($newpath); + } + + $file = new DriveFile(); + $file->setName($newName); + $opts = [ + 'fields' => self::FETCHFIELDS_GET + ]; + if ($newParent !== $oldParent) { + $opts['addParents'] = $newParent; + if ($oldParent !== '') { + $opts['removeParents'] = $oldParent; + } + } + + try { + $updatedFile = $this->service->files->update($fileId, $file, $this->applyDefaultParams($opts, 'files.update')); + + $id = $updatedFile->getId(); + if (isset($this->cacheHasDirs[$fileId])) { + $this->cacheHasDirs[$id] = $this->cacheHasDirs[$fileId]; + } + $this->uncacheId($fileId); + $this->cacheFileObjects[$id] = $updatedFile; + $this->cacheObjects([$id => $updatedFile]); + $this->resetRequest([$oldParent, $newParent, $fileId, $id]); + + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function copy($path, $newpath) + { + $this->refreshToken(); + if ($this->useDisplayPaths) { + $srcId = $this->toVirtualPath($path, false, true); + $newpathDir = self::dirname($newpath); + $toPath = $this->toSingleVirtualPath($newpathDir, false, false, true, true); + if ($toPath === false) { + return false; + } + if ($toPath === '') { + $toPath = $this->root; + } + $newParentId = $toPath; + $fileName = basename($newpath); + } else { + [, $srcId] = $this->splitPath($path); + [$newParentId, $fileName] = $this->splitPath($newpath); + } + + $file = new DriveFile(); + $file->setName($fileName); + $file->setParents([ + $newParentId + ]); + + $newFile = $this->service->files->copy($srcId, $file, $this->applyDefaultParams([ + 'fields' => self::FETCHFIELDS_GET + ], 'files.copy')); + + if ($newFile instanceof DriveFile) { + $id = $newFile->getId(); + $this->cacheFileObjects[$id] = $newFile; + $this->cacheObjects([$id => $newFile]); + if (isset($this->cacheHasDirs[$srcId])) { + $this->cacheHasDirs[$id] = $this->cacheHasDirs[$srcId]; + } + + if ($this->getRawVisibility($srcId) === AdapterInterface::VISIBILITY_PUBLIC) { + $this->publish($id); + } else { + $this->unPublish($id); + } + $this->resetRequest([$id, $newParentId]); + return true; + } + + return false; + } + + /** + * Delete an array of google file ids + * + * @param string[]|string $ids + * @return bool + */ + protected function delete_by_id($ids) + { + $this->refreshToken(); + $deleted = false; + if (!is_array($ids)) { + $ids = [$ids]; + } + foreach ($ids as $id) { + if ($id !== '' && ($file = $this->getFileObject($id))) { + if ($file->getParents()) { + if ($this->usePermanentDelete && $this->service->files->delete($id, $this->applyDefaultParams([], 'files.delete'))) { + $this->uncacheId($id); + $deleted = true; + } else { + if (!$this->usePermanentDelete) { + $file = new DriveFile(); + $file->setTrashed(true); + if ($this->service->files->update($id, $file, $this->applyDefaultParams([], 'files.update'))) { + $this->uncacheId($id); + $deleted = true; + } + } + } + } + } + } + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function delete($path) + { + if ($path === '' || $path === '/') { + return false; + } // do not allow deleting root... + + $deleted = false; + if ($this->useDisplayPaths) { + try { + $ids = $this->toVirtualPath($path, false); + $deleted = $this->delete_by_id($ids); + } catch (\Exception $e) { + //Unnecesary + } + } else { + if ($file = $this->getFileObject($path)) { + $deleted = $this->delete_by_id($file->getId()); + } + } + + if ($deleted) { + $this->resetRequest('', true); + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function deleteDir($dirname) + { + return $this->delete($dirname); + } + + /** + * {@inheritdoc} + */ + public function createDir($dirname, Config $config, $internalCall = false) + { + try { + $meta = $this->getMetadata($dirname); + } catch (FileNotFoundException $e) { + $meta = false; + } + + if ($meta !== false) { + return [ + 'path' => $meta['path'], + 'filename' => $meta['filename'], + 'extension' => $meta['extension'] + ]; + } + + [$pdir, $name] = $this->splitPath($dirname, false); + if ($this->useDisplayPaths) { + if ($pdir !== $this->root) { + $pdir = $this->toSingleVirtualPath($pdir, false, false, true, true); // recursion! + if ($pdir === false) { + return false; + } // failed to create dirs + } + } + + $folder = $this->createDirectory($name, $pdir !== '' ? basename($pdir) : $pdir); + if ($folder !== null) { + $itemId = $folder->getId(); + $this->cacheFileObjects[$itemId] = $folder; + $this->cacheHasDirs[$itemId] = false; + $this->cacheObjects([$itemId => $folder]); + $path_parts = $this->splitFileExtension($name); + $result = [ + 'path' => Util::normalizeDirname($pdir).'/'.($this->useDisplayPaths ? $name : $itemId), + 'filename' => $path_parts['filename'], + 'extension' => $path_parts['extension'] + ]; + return $result; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function has($path) + { + if ($this->useDisplayPaths) { + try { + $this->toVirtualPath($path, false); + return true; + } catch (FileNotFoundException $e) { + return false; + } + } + return ($this->getFileObject($path, true) instanceof DriveFile); + } + + /** + * {@inheritdoc} + */ + public function read($path) + { + $this->refreshToken(); + if ($this->useDisplayPaths) { + $fileId = $this->toVirtualPath($path, false, true); + } else { + [, $fileId] = $this->splitPath($path); + } + /** @var RequestInterface $response */ + if (($response = $this->service->files->get(/** @scrutinizer ignore-type */ $fileId, $this->applyDefaultParams(['alt' => 'media'], 'files.get')))) { + return [ + 'contents' => (string)$response->getBody() + ]; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function readStream($path) + { + $this->refreshToken(); + if ($this->useDisplayPaths) { + $path = $this->toVirtualPath($path, false, true); + } + + $redirect = null; + if (func_num_args() > 1) { + $redirect = func_get_arg(1); + } + + if (!$redirect) { + $redirect = [ + 'cnt' => 0, + 'url' => '', + 'token' => '', + 'cookies' => [] + ]; + if (($file = $this->getFileObject($path))) { + if ($file->getMimeType() === self::DIRMIME) { + throw new FileNotFoundException($path); + } + $dlurl = $this->getDownloadUrl($file); + $client = $this->service->getClient(); + /** @var array|string|object $token */ + if ($client->isUsingApplicationDefaultCredentials()) { + $token = $client->fetchAccessTokenWithAssertion(); + } else { + $token = $client->getAccessToken(); + } + $access_token = ''; + if (is_array($token)) { + if (empty($token['access_token']) && !empty($token['refresh_token'])) { + $token = $client->fetchAccessTokenWithRefreshToken(); + } + $access_token = $token['access_token']; + } else { + if (($token = @json_decode($token))) { + $access_token = $token->access_token; + } + } + $redirect = [ + 'cnt' => 0, + 'url' => '', + 'token' => $access_token, + 'cookies' => [] + ]; + } + } else { + if ($redirect['cnt'] > 5) { + return false; + } + $dlurl = $redirect['url']; + $redirect['url'] = ''; + $access_token = $redirect['token']; + } + + if (!empty($dlurl)) { + $url = parse_url($dlurl); + $cookies = []; + if ($redirect['cookies']) { + foreach ($redirect['cookies'] as $d => $c) { + if (strpos($url['host'], $d) !== false) { + $cookies[] = $c; + } + } + } + if (!empty($access_token)) { + $query = isset($url['query']) ? '?'.$url['query'] : ''; + $stream = stream_socket_client('ssl://'.$url['host'].':443'); + stream_set_timeout($stream, 300); + fwrite($stream, "GET {$url['path']}{$query} HTTP/1.1\r\n"); + fwrite($stream, "Host: {$url['host']}\r\n"); + fwrite($stream, "Authorization: Bearer {$access_token}\r\n"); + fwrite($stream, "Connection: Close\r\n"); + if ($cookies) { + fwrite($stream, 'Cookie: '.implode('; ', $cookies)."\r\n"); + } + fwrite($stream, "\r\n"); + while (($res = trim(fgets($stream))) !== '') { + // find redirect + if (preg_match('/^Location: (.+)$/', $res, $m)) { + $redirect['url'] = $m[1]; + } + // fetch cookie + if (strpos($res, 'Set-Cookie:') === 0) { + $domain = $url['host']; + if (preg_match('/^Set-Cookie:(.+)(?:domain=\s*([^ ;]+))?/i', $res, $c1)) { + if (!empty($c1[2])) { + $domain = trim($c1[2]); + } + if (preg_match('/([^ ]+=[^;]+)/', $c1[1], $c2)) { + $redirect['cookies'][$domain] = $c2[1]; + } + } + } + } + if ($redirect['url']) { + $redirect['cnt']++; + fclose($stream); + return $this->readStream($path, $redirect); + } + return compact('stream'); + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function listContents($directory = '', $recursive = false) + { + $this->refreshToken(); + if ($this->useDisplayPaths) { + $time = microtime(true); + + try { + $vp = $this->toVirtualPath($directory); + } catch (\Exception $e) { + $vp = []; + } + $elapsed = (microtime(true) - $time) * 1000.0; + if (!is_array($vp)) { + $vp = [$vp]; + } + + $items = []; + foreach ($vp as $path) { + if (DEBUG_ME) { + echo 'Converted display path to virtual path ['.number_format($elapsed, 1).'ms]: '.$path."\n"; + } + $items = array_merge($items, array_values($this->getItems($path, $recursive))); + } + } else { + $items = array_values($this->getItems($directory, $recursive)); + } + return $items; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($path) + { + if ($this->useDisplayPaths) { + $path = $this->toVirtualPath($path, true, true); + } + if (($obj = $this->getFileObject($path, true))) { + if ($obj instanceof DriveFile) { + return $this->normaliseObject($obj, self::dirname($path)); + } + } + return false; + } + + /** + * {@inheritdoc} + */ + public function getSize($path) + { + $meta = $this->getMetadata($path); + return ($meta && isset($meta['size'])) ? $meta : false; + } + + /** + * {@inheritdoc} + */ + public function getMimetype($path) + { + $meta = $this->getMetadata($path); + return ($meta && isset($meta['mimetype'])) ? $meta : false; + } + + /** + * {@inheritdoc} + */ + public function getTimestamp($path) + { + $meta = $this->getMetadata($path); + return ($meta && isset($meta['timestamp'])) ? $meta : false; + } + + /** + * {@inheritdoc} + */ + public function setVisibility($path, $visibility, $internalCall = false) + { + if ($this->useDisplayPaths && !$internalCall) { + try { + $path = $this->toVirtualPath($path, false, true); + } catch (\Exception $e) { + return false; + } + } + $result = ($visibility === AdapterInterface::VISIBILITY_PUBLIC) ? $this->publish($path) : $this->unPublish($path); + + if ($result) { + return compact('path', 'visibility'); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getVisibility($path) + { + if ($this->useDisplayPaths) { + $path = $this->toVirtualPath($path, false, true); + } + return [ + 'visibility' => $this->getRawVisibility($path) + ]; + } + + // /////////////////- ORIGINAL METHODS -/////////////////// + + /** + * Get contents parmanent URL + * + * @param string $path itemId path + * @param string $path itemId path + */ + public function getUrl($path) + { + if ($this->useDisplayPaths) { + $path = $this->toVirtualPath($path, false, true); + } + if ($this->publish(/** @scrutinizer ignore-type */ $path)) { + $obj = $this->getFileObject($path); + if (($url = $obj->getWebContentLink())) { + return str_replace('export=download', 'export=media', $url); + } + if (($url = $obj->getWebViewLink())) { + return $url; + } + } + return false; + } + + /** + * Has child directory + * + * @param string $path itemId path + * @return array + */ + public function hasDir($path) + { + $meta = $this->getMetadata($path); + return ($meta && isset($meta['hasdir'])) ? $meta : [ + 'hasdir' => true + ]; + } + + /** + * Do cache cacheHasDirs with batch request + * + * @param array $targets [[path => id],...] + * @param array $object + * @return array + */ + protected function setHasDir($targets, $object) + { + $this->refreshToken(); + $service = $this->service; + $client = $service->getClient(); + $gFiles = $service->files; + + $opts = [ + 'pageSize' => 1, + 'orderBy' => 'folder,modifiedTime,name', + ]; + + $paths = []; + $client->setUseBatch(true); + $batch = $service->createBatch(); + $i = 0; + foreach ($targets as $id) { + $opts['q'] = sprintf('trashed = false and "%s" in parents and mimeType = "%s"', $id, self::DIRMIME); + /** @var RequestInterface $request */ + $request = $gFiles->listFiles($this->applyDefaultParams($opts, 'files.list')); + $key = ++$i; + $batch->add($request, (string)$key); + $paths['response-'.$key] = $id; + } + $results = $batch->execute(); + foreach ($results as $key => $result) { + if ($result instanceof FileList) { + $object[$paths[$key]]['hasdir'] = $this->cacheHasDirs[$paths[$key]] = (bool)$result->getFiles(); + } + } + $client->setUseBatch(false); + return $object; + } + + /** + * Get the object permissions presented as a visibility. + * + * @param string $path itemId path + * @return string + */ + protected function getRawVisibility($path) + { + $file = $this->getFileObject($path); + $permissions = $file->getPermissions(); + $visibility = AdapterInterface::VISIBILITY_PRIVATE; + foreach ($permissions as $permission) { + if ($permission->type === $this->publishPermission['type'] && $permission->role === $this->publishPermission['role']) { + $visibility = AdapterInterface::VISIBILITY_PUBLIC; + break; + } + } + return $visibility; + } + + /** + * Publish specified path item + * + * @param string $path itemId path + * @return bool + */ + protected function publish($path) + { + $this->refreshToken(); + if (($file = $this->getFileObject($path))) { + if ($this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC) { + return true; + } + try { + $new_permission = new Permission($this->publishPermission); + if ($permission = $this->service->permissions->create($file->getId(), $new_permission, $this->applyDefaultParams([], 'files.create'))) { + $file->setPermissions([$permission]); + return true; + } + } catch (\Exception $e) { + return false; + } + } + + return false; + } + + /** + * Un-publish specified path item + * + * @param string $path itemId path + * @return bool + */ + protected function unPublish($path) + { + $this->refreshToken(); + if (($file = $this->getFileObject($path))) { + $permissions = $file->getPermissions(); + try { + foreach ($permissions as $permission) { + if ($permission->type === $this->publishPermission['type'] && $permission->role === $this->publishPermission['role'] && !empty($file->getId())) { + $this->service->permissions->delete($file->getId(), $permission->getId(), $this->applyDefaultParams([], 'files.trash')); + } + } + $file->setPermissions([]); + return true; + } catch (\Exception $e) { + return false; + } + } + + return false; + } + + /** + * Path splits to dirId, fileId or newName + * + * @param string $path + * @param bool $getParentId True => return only parent id, False => return full path (basically the same as dirname($path)) + * @return array [ $dirId , $fileId|newName ] + */ + protected function splitPath($path, $getParentId = true) + { + if ($path === '' || $path === '/') { + $fileName = $this->root; + $dirName = ''; + } else { + $paths = explode('/', $path); + $fileName = array_pop($paths); + if ($getParentId) { + $dirName = $paths ? array_pop($paths) : ''; + } else { + $dirName = implode('/', $paths); + } + if ($dirName === '') { + $dirName = $this->root; + } + } + return [ + $dirName, + $fileName + ]; + } + + /** + * Item name splits to filename and extension + * This function supported include '/' in item name + * + * @param string $name + * @return array [ 'filename' => $filename , 'extension' => $extension ] + */ + protected function splitFileExtension($name) + { + $name_parts = explode('.', $name); + $extension = isset($name_parts[1]) ? array_pop($name_parts) : ''; + $filename = implode('.', $name_parts); + return compact('filename', 'extension'); + } + + /** + * Get normalised files array from DriveFile + * + * @param DriveFile $object + * @param string $dirname Parent directory itemId path + * @return array Normalised files array + */ + protected function normaliseObject(DriveFile $object, $dirname) + { + $id = $object->getId(); + $path_parts = $this->splitFileExtension($object->getName()); + $result = ['id' => $id, 'visibility' => AdapterInterface::VISIBILITY_PRIVATE]; + $result['type'] = $object->mimeType === self::DIRMIME ? 'dir' : 'file'; + $permissions = $object->getPermissions(); + try { + foreach ($permissions as $permission) { + if ($permission->type === $this->publishPermission['type'] && $permission->role === $this->publishPermission['role']) { + $result['visibility'] = AdapterInterface::VISIBILITY_PUBLIC; + break; + } + } + } catch (\Exception $e) { + // Unnecesary + } + if ($this->useDisplayPaths) { + $result['virtual_path'] = ($dirname ? ($dirname.'/') : '').$id; + $result['display_path'] = $this->toDisplayPath($result['virtual_path']); + + $result['path'] = $result['display_path']; + } else { + $result['virtual_path'] = ($dirname ? ($dirname.'/') : '').$id; + $result['display_path'] = $this->toDisplayPath($result['virtual_path']); + + $result['path'] = $result['virtual_path']; + } + + $result['filename'] = $path_parts['filename']; + $result['extension'] = $path_parts['extension']; + $result['timestamp'] = strtotime($object->getModifiedTime()); + if ($result['type'] === 'file') { + $result['mimetype'] = $object->mimeType; + $result['size'] = (int)$object->getSize(); + } + if ($result['type'] === 'dir') { + $result['size'] = 0; + if ($this->useHasDir) { + $result['hasdir'] = isset($this->cacheHasDirs[$id]) ? $this->cacheHasDirs[$id] : false; + } + } + return $result; + } + + /** + * Get items array of target dirctory + * + * @param string $dirname itemId path + * @param bool $recursive + * @param int $maxResults + * @param string $query + * @return array Items array + */ + protected function getItems($dirname, $recursive = false, $maxResults = 0, $query = '') + { + $this->refreshToken(); + [, $itemId] = $this->splitPath($dirname); + + $maxResults = min($maxResults, 1000); + $results = []; + $parameters = [ + 'pageSize' => $maxResults ?: 1000, + 'fields' => self::FETCHFIELDS_LIST, + 'orderBy' => 'folder,modifiedTime,name', + 'spaces' => $this->spaces, + 'q' => sprintf('trashed = false and "%s" in parents', $itemId) + ]; + if ($query) { + $parameters['q'] .= ' and ('.$query.')'; + } + $pageToken = null; + $gFiles = $this->service->files; + $this->cacheHasDirs[$itemId] = false; + $setHasDir = []; + + do { + try { + if ($pageToken) { + $parameters['pageToken'] = $pageToken; + } + $fileObjs = $gFiles->listFiles($this->applyDefaultParams($parameters, 'files.list')); + if ($fileObjs instanceof FileList) { + foreach ($fileObjs as $obj) { + $id = $obj->getId(); + $this->cacheFileObjects[$id] = $obj; + $result = $this->normaliseObject($obj, $dirname); + $results[$id] = $result; + if ($result['type'] === 'dir') { + if ($this->useHasDir) { + $setHasDir[$id] = $id; + } + if ($this->cacheHasDirs[$itemId] === false) { + $this->cacheHasDirs[$itemId] = true; + unset($setHasDir[$itemId]); + } + if ($recursive) { + $results = array_merge($results, $this->getItems($result['virtual_path'], true, $maxResults, $query)); + } + } + } + $pageToken = $fileObjs->getNextPageToken(); + } else { + $pageToken = null; + } + } catch (\Exception $e) { + $pageToken = null; + } + } while ($pageToken && $maxResults === 0); + + if ($setHasDir) { + $results = $this->setHasDir($setHasDir, $results); + } + return array_values($results); + } + + /** + * Get file object DriveFile + * + * @param string $path itemId path + * @param bool $checkDir do check hasdir + * @return DriveFile|null + */ + public function getFileObject($path, $checkDir = false) + { + [, $itemId] = $this->splitPath($path); + if (isset($this->cacheFileObjects[$itemId])) { + return $this->cacheFileObjects[$itemId]; + } + $this->refreshToken(); + $service = $this->service; + $client = $service->getClient(); + + $client->setUseBatch(true); + try { + $batch = $service->createBatch(); + + $opts = [ + 'fields' => self::FETCHFIELDS_GET + ]; + + /** @var RequestInterface $request */ + $request = $this->service->files->get($itemId, $opts); + $batch->add($request, 'obj'); + + if ($checkDir && $this->useHasDir) { + /** @var RequestInterface $request */ + $request = $service->files->listFiles($this->applyDefaultParams([ + 'pageSize' => 1, + 'orderBy' => 'folder,modifiedTime,name', + 'q' => sprintf('trashed = false and "%s" in parents and mimeType = "%s"', $itemId, self::DIRMIME) + ], 'files.list')); + + $batch->add($request, 'hasdir'); + } + $results = array_values($batch->execute()); + + [$fileObj, $hasdir] = array_pad($results, 2, null); + } finally { + $client->setUseBatch(false); + } + + if ($fileObj instanceof DriveFile) { + if ($hasdir && $fileObj->mimeType === self::DIRMIME) { + if ($hasdir instanceof FileList) { + $this->cacheHasDirs[$fileObj->getId()] = (bool)$hasdir->getFiles(); + } + } + } else { + $fileObj = null; + } + + if ($fileObj !== null) { + $this->cacheFileObjects[$itemId] = $fileObj; + $this->cacheObjects([$itemId => $fileObj]); + } + + return $fileObj; + } + + /** + * Get download url + * + * @param DriveFile $file + * @return string|false + */ + protected function getDownloadUrl($file) + { + if (strpos($file->mimeType, 'application/vnd.google-apps') !== 0) { + $params = $this->applyDefaultParams(['alt' => 'media'], 'files.get'); + return 'https://www.googleapis.com/drive/v3/files/'.$file->getId().'?'.http_build_query($params); + } + + $mimeMap = $this->options['appsExportMap']; + if (isset($mimeMap[$file->getMimeType()])) { + $mime = $mimeMap[$file->getMimeType()]; + } else { + $mime = $mimeMap['default']; + } + $mime = rawurlencode($mime); + + $params = $this->applyDefaultParams(['mimeType' => $mime], 'files.get'); + return 'https://www.googleapis.com/drive/v3/files/'.$file->getId().'/export?'.http_build_query($params); + } + + /** + * Create directory + * + * @param string $name + * @param string $parentId + * @return DriveFile|null + */ + protected function createDirectory($name, $parentId) + { + $this->refreshToken(); + $file = new DriveFile(); + $file->setName($name); + $file->setParents([ + $parentId + ]); + $file->setMimeType(self::DIRMIME); + + $obj = $this->service->files->create($file, $this->applyDefaultParams([ + 'fields' => self::FETCHFIELDS_GET + ], 'files.create')); + $this->resetRequest($parentId); + + return ($obj instanceof DriveFile) ? $obj : null; + } + + /** + * Upload|Update item + * + * @param string $path + * @param string|resource $contents + * @param Config $config + * @param bool|null $updating If null then we check for existence of the file + * @return array|false item info array + */ + protected function upload($path, $contents, Config $config, $updating = null) + { + $this->refreshToken(); + [$parentId, $fileName] = $this->splitPath($path); + $mime = $config->get('mimetype'); + $file = new DriveFile(); + + if ($updating === null || $updating === true) { + $srcFile = $this->getFileObject($path); + $updating = $srcFile !== null; + } else { + $srcFile = null; + } + if (!$updating) { + $file->setName($fileName); + $file->setParents([ + $parentId + ]); + } + + if (!$mime) { + $mime = Util::guessMimeType($fileName, is_string($contents) ? $contents : ''); + if (empty($mime)) { + $mime = 'application/octet-stream'; + } + } + $file->setMimeType($mime); + + /** @var StreamInterface $stream */ + if (function_exists('\GuzzleHttp\Psr7\stream_for')) { + $stream = \GuzzleHttp\Psr7\stream_for($contents); + } else { + $stream = \GuzzleHttp\Psr7\Utils::streamFor($contents); + } + $size = $stream->getSize(); + + if ($size <= self::MAX_CHUNK_SIZE) { + // one shot upload + $params = [ + 'data' => $stream, + 'uploadType' => 'media', + 'fields' => self::FETCHFIELDS_GET + ]; + + if (!$updating) { + $obj = $this->service->files->create($file, $this->applyDefaultParams($params, 'files.create')); + } else { + $obj = $this->service->files->update($srcFile->getId(), $file, $this->applyDefaultParams($params, 'files.update')); + } + } else { + // chunked upload + $client = $this->service->getClient(); + + $params = [ + 'fields' => self::FETCHFIELDS_GET + ]; + + $client->setDefer(true); + if (!$updating) { + /** @var RequestInterface $request */ + $request = $this->service->files->create($file, $this->applyDefaultParams($params, 'files.create')); + } else { + /** @var RequestInterface $request */ + $request = $this->service->files->update($srcFile->getId(), $file, $this->applyDefaultParams($params, 'files.update')); + } + + $media = new StreamableUpload($client, $request, $mime, $stream, true, self::MAX_CHUNK_SIZE); + $media->setFileSize($size); + do { + if (DEBUG_ME) { + echo "* Uploading next chunk.\n"; + } + $status = $media->nextChunk(); + } while ($status === false); + + // The final value of $status will be the data from the API for the object that has been uploaded. + if ($status !== false) { + $obj = $status; + } + + $client->setDefer(false); + } + + $this->resetRequest($parentId); + + if (isset($obj) && $obj instanceof DriveFile) { + $this->cacheFileObjects[$obj->getId()] = $obj; + $this->cacheObjects([$obj->getId() => $obj]); + $result = $this->normaliseObject($obj, self::dirname($path)); + + if (($visibility = $config->get('visibility'))) { + if ($this->setVisibility($result['virtual_path'], $visibility, true)) { + $result['visibility'] = $visibility; + } + } + + return $result; + } + return false; + } + + /** + * @param array $ids + * @param bool $checkDir + * @return array + */ + protected function getObjects($ids, $checkDir = false) + { + if ($checkDir && !$this->useHasDir) { + $checkDir = false; + } + + $fetch = []; + foreach ($ids as $itemId) { + if (!isset($this->cacheFileObjects[$itemId])) { + $fetch[$itemId] = null; + } + } + if (!empty($fetch) || $checkDir) { + $this->refreshToken(); + $service = $this->service; + $client = $service->getClient(); + + $client->setUseBatch(true); + try { + $batch = $service->createBatch(); + + $opts = [ + 'fields' => self::FETCHFIELDS_GET + ]; + + $count = 0; + if (!$this->rootId) { + /** @var RequestInterface $request */ + $request = $this->service->files->get($this->root, $this->applyDefaultParams($opts, 'files.get')); + $batch->add($request, 'rootdir'); + $count++; + } + + $results = []; + foreach ($fetch as $itemId => $value) { + if (DEBUG_ME) { + echo "*** FETCH *** $itemId\n"; + } + + /** @var RequestInterface $request */ + $request = $this->service->files->get($itemId, $opts); + $batch->add($request, $itemId); + $count++; + + if ($checkDir) { + /** @var RequestInterface $request */ + $request = $service->files->listFiles($this->applyDefaultParams([ + 'pageSize' => 1, + 'orderBy' => 'folder,modifiedTime,name', + 'q' => sprintf('trashed = false and "%s" in parents and mimeType = "%s"', $itemId, self::DIRMIME) + ], 'files.list')); + $batch->add($request, 'hasdir-'.$itemId); + $count++; + } + + if ($count > 90) { + // batch requests are limited to 100 calls in a single batch request + $results[] = $batch->execute(); + $batch = $service->createBatch(); + $count = 0; + } + } + if ($count > 0) { + $results[] = $batch->execute(); + } + if (!empty($results)) { + $results = array_merge(...$results); + } + + foreach ($results as $key => $value) { + if ($value instanceof DriveFile) { + $itemId = $value->getId(); + $this->cacheFileObjects[$itemId] = $value; + if (!$this->rootId && strcmp($key, 'response-rootdir') === 0) { + $this->rootId = $itemId; + } + } else { + if ($checkDir && $value instanceof FileList) { + if (strncmp($key, 'response-hasdir-', 16) === 0) { + $key = substr($key, 16); + if (isset($this->cacheFileObjects[$key]) && $this->cacheFileObjects[$key]->mimeType === self::DIRMIME) { + $this->cacheHasDirs[$key] = (bool)$value->getFiles(); + } + } + } + } + } + + $this->cacheObjects($results); + } finally { + $client->setUseBatch(false); + } + } + + $objects = []; + foreach ($ids as $itemId) { + $objects[$itemId] = isset($this->cacheFileObjects[$itemId]) ? $this->cacheFileObjects[$itemId] : null; + } + return $objects; + } + + protected function buildPathFromCacheFileObjects($lastItemId) + { + $complete_paths = []; + $itemIds = [$lastItemId]; + $paths = ['' => '']; + $is_first = true; + while (!empty($itemIds)) { + $new_itemIds = []; + $new_paths = []; + foreach ($itemIds as $itemId) { + if (empty($this->cacheFileObjects[$itemId])) { + continue; + } + + /* @var DriveFile $obj */ + $obj = $this->cacheFileObjects[$itemId]; + $parents = $obj->getParents(); + + foreach ($paths as $id => $path) { + if ($is_first) { + $is_first = false; + $new_path = $this->sanitizeFilename($obj->getName()); + $id = $itemId; + } else { + $new_path = $this->sanitizeFilename($obj->getName()).'/'.$path; + } + + if ($this->rootId === $itemId) { + if (!empty($path)) { + $complete_paths[$id] = $path; + } // this path is complete...don't include drive name + } else { + if (!empty($parents)) { + $new_paths[$id] = $new_path; + } + } + } + + if (!empty($parents)) { + $new_itemIds[] = (array)($obj->getParents()); + } + } + $paths = $new_paths; + $itemIds = !empty($new_itemIds) ? array_merge(...$new_itemIds) : []; + } + return $complete_paths; + } + + public function uncacheFolder($path) + { + if ($this->useDisplayPaths) { + try { + $path_id = $this->getCachedPathId($path); + if (is_array($path_id) && !empty($path_id[0] ?? null)) { + $this->uncacheId($path_id[0]); + } + } catch (FileNotFoundException $e) { + // unnecesary + } + } else { + $this->uncacheId($path); + } + } + + protected function uncacheId($id) + { + if (empty($id)) { + return; + } + $basePath = null; + foreach ($this->cachedPaths as $path => $itemId) { + if ($itemId === $id) { + $basePath = (string)$path; + break; + } + } + if ($basePath) { + foreach ($this->cachedPaths as $path => $itemId) { + if (strlen((string)$path) >= strlen($basePath) && strncmp((string)$path, $basePath, strlen($basePath)) === 0) { + unset($this->cachedPaths[$path]); + } + } + } + + unset($this->cacheFileObjects[$id], $this->cacheHasDirs[$id]); + } + + protected function cacheObjects($objects) + { + foreach ($objects as $key => $value) { + if ($value instanceof DriveFile) { + $complete_paths = $this->buildPathFromCacheFileObjects($value->getId()); + foreach ($complete_paths as $itemId => $path) { + if (DEBUG_ME) { + echo 'Complete path: '.$path.' ['.$itemId."]\n"; + } + + if (!isset($this->cachedPaths[$path])) { + $this->cachedPaths[$path] = $itemId; + } else { + if (!is_array($this->cachedPaths[$path])) { + if ($itemId !== $this->cachedPaths[$path]) { + // convert to array + $this->cachedPaths[$path] = [ + $this->cachedPaths[$path], + $itemId + ]; + + if (DEBUG_ME) { + echo 'Caching [DUP]: '.$path.' => '.$itemId."\n"; + } + } + } else { + if (!in_array($itemId, $this->cachedPaths[$path])) { + array_push($this->cachedPaths[$path], $itemId); + if (DEBUG_ME) { + echo 'Caching [DUP]: '.$path.' => '.$itemId."\n"; + } + } + } + } + } + } + } + } + + protected function indexString($str, $ch = '/') + { + $indices = []; + for ($i = 0, $len = strlen($str); $i < $len; $i++) { + if ($str[$i] === $ch) { + $indices[] = $i; + } + } + return $indices; + } + + protected function getCachedPathId($path, $indices = null) + { + $pathLen = strlen($path); + if ($indices === null) { + $indices = $this->indexString($path, '/'); + $indices[] = $pathLen; + } + + $maxLen = 0; + $itemId = null; + $pathMatch = null; + + foreach ($this->cachedPaths as $pathFrag => $id) { + $pathFrag = (string)$pathFrag; + $len = strlen($pathFrag); + if ($len > $pathLen || $len < $maxLen || !in_array($len, $indices)) { + continue; + } + + if (strncmp($pathFrag, $path, $len) === 0) { + if ($len === $pathLen) { + return [$id, $pathFrag]; + } // we found a perfect match + + $maxLen = $len; + $itemId = $id; + $pathMatch = $pathFrag; + } + } + + // we found a partial match or none at all + return [$itemId, $pathMatch]; + } + + protected function getPathToIndex($path, $i, $indices) + { + if ($i < 0) { + return ''; + } + if (!isset($indices[$i]) || !isset($indices[$i + 1])) { + return $path; + } + return substr($path, 0, $indices[$i]); + } + + protected function getToken($path, $i, $indices) + { + if ($i < 0 || !isset($indices[$i])) { + return ''; + } + $start = $i > 0 ? $indices[$i - 1] + 1 : 0; + return substr($path, $start, isset($indices[$i]) ? $indices[$i] - $start : null); + } + + protected function cachePaths($displayPath, $i, $indices, $parentItemId) + { + $nextItemId = $parentItemId; + for ($count = count($indices); $i < $indices; $i++) { + $token = $this->getToken($displayPath, $i, $indices); + if (empty($token) && $token !== '0') { + return; + } + $basePath = $this->getPathToIndex($displayPath, $i - 2, $indices); + if (!empty($basePath)) { + $basePath .= '/'; + } + + if ($nextItemId === null) { + return; + } + + $is_last = $i === $count - 1; + + // search only for directories unless it's the last token + if (!is_array($nextItemId)) { + $nextItemId = [$nextItemId]; + } + + $items = []; + foreach ($nextItemId as $id) { + if (!$this->canRequest($id, $is_last)) { + continue; + } + $this->markRequest($id, $is_last); + if (DEBUG_ME) { + echo 'New req: '.$id; + } + $items[] = $this->getItems($id, false, 0, $is_last ? '' : 'mimeType = "'.self::DIRMIME.'"'); + if (DEBUG_ME) { + echo " ...done\n"; + } + } + if (!empty($items)) { + /** @noinspection SlowArrayOperationsInLoopInspection */ + $items = array_merge(...$items); + } + + $nextItemId = null; + foreach ($items as $item) { + $itemId = basename($item['virtual_path']); + $fullPath = $basePath.$item['display_path']; + + // update cache + if (!isset($this->cachedPaths[$fullPath])) { + $this->cachedPaths[$fullPath] = $itemId; + if (DEBUG_ME) { + echo 'Caching: '.$fullPath.' => '.$itemId."\n"; + } + } else { + if (!is_array($this->cachedPaths[$fullPath])) { + if ($itemId !== $this->cachedPaths[$fullPath]) { + // convert to array + $this->cachedPaths[$fullPath] = [ + $this->cachedPaths[$fullPath], + $itemId + ]; + + if (DEBUG_ME) { + echo 'Caching [DUP]: '.$fullPath.' => '.$itemId."\n"; + } + } + } else { + if (!in_array($itemId, $this->cachedPaths[$fullPath])) { + $this->cachedPaths[$fullPath][] = $itemId; + if (DEBUG_ME) { + echo 'Caching [DUP]: '.$fullPath.' => '.$itemId."\n"; + } + } + } + } + + if (basename($item['display_path']) === $token) { + $nextItemId = $this->cachedPaths[$fullPath]; + } // found our token + } + } + } + + /** + * Create a full virtual path from cache + * + * @param string $displayPath + * @param bool $returnFirstItem return first item only + * @return string[]|string + * + * @throws FileNotFoundException + */ + protected function makeFullVirtualPath($displayPath, $returnFirstItem = false) + { + $paths = ['' => null]; + + $tmp = ''; + $tokens = explode('/', trim($displayPath, '/')); + foreach ($tokens as $token) { + if (empty($tmp)) { + $tmp .= $token; + } else { + $tmp .= '/'.$token; + } + + if (empty($this->cachedPaths[$tmp])) { + throw new FileNotFoundException($displayPath); + } + if (is_array($this->cachedPaths[$tmp])) { + $new_paths = []; + foreach ($paths as $path => $obj) { + $parentId = $path === '' ? '' : basename($path); + foreach ($this->cachedPaths[$tmp] as $id) { + if ($parentId === '' || (!empty($this->cacheFileObjects[$id]->parents) && in_array($parentId, $this->cacheFileObjects[$id]->parents))) { + $new_paths[$path.'/'.$id] = $this->cacheFileObjects[$id]; + } + } + } + $paths = $new_paths; + } else { + $id = $this->cachedPaths[$tmp]; + $new_paths = []; + foreach ($paths as $path => $obj) { + $parentId = $path === '' ? '' : basename($path); + if ($parentId === '' || (!empty($this->cacheFileObjects[$id]->parents) && in_array($parentId, $this->cacheFileObjects[$id]->parents))) { + $new_paths[$path.'/'.$id] = $this->cacheFileObjects[$id]; + } + } + $paths = $new_paths; + } + } + + $count = count($paths); + if ($count === 0) { + throw new FileNotFoundException($displayPath); + } + + if (count($paths) > 1) { + // sort oldest to newest + uasort($paths, function ($a, $b) { + $t1 = strtotime($a->getCreatedTime()); + $t2 = strtotime($b->getCreatedTime()); + if ($t1 < $t2) { + return -1; + } + if ($t1 > $t2) { + return 1; + } + return 0; + }); + + if (!$returnFirstItem) { + return array_keys($paths); + } + } + return array_keys($paths)[0]; + } + + protected function returnSingle($item, $returnFirstItem) + { + if ($returnFirstItem && is_array($item)) { + return $item[0]; + } + return $item; + } + + /** + * Convert display path to virtual path or just id + * + * @param string $displayPath + * @param bool $makeFullVirtualPath + * @param bool $returnFirstItem + * @return string[]|string Single itemId/path or array of them + * + * @throws FileNotFoundException + */ + protected function toVirtualPath($displayPath, $makeFullVirtualPath = true, $returnFirstItem = false) + { + if ($displayPath === '' || $displayPath === '/' || $displayPath === $this->root) { + return ''; + } + + $displayPath = trim($displayPath, '/'); // not needed + + $indices = $this->indexString($displayPath, '/'); + $indices[] = strlen($displayPath); + + [$itemId, $pathMatch] = $this->getCachedPathId($displayPath, $indices); + $i = 0; + if ($pathMatch !== null) { + if (strcmp($pathMatch, $displayPath) === 0) { + if ($makeFullVirtualPath) { + return $this->makeFullVirtualPath($displayPath, $returnFirstItem); + } + return $this->returnSingle($itemId, $returnFirstItem); + } + $i = array_search(strlen($pathMatch), $indices) + 1; + } + if ($itemId === null) { + $itemId = ''; + } + $this->cachePaths($displayPath, $i, $indices, $itemId); + + if ($makeFullVirtualPath) { + return $this->makeFullVirtualPath($displayPath, $returnFirstItem); + } + + if (empty($this->cachedPaths[$displayPath])) { + throw new FileNotFoundException($displayPath); + } + + return $this->returnSingle($this->cachedPaths[$displayPath], $returnFirstItem); + } + + /** + * Convert virtual path to display path + * + * @param string $virtualPath + * @return string + * + * @throws FileNotFoundException + */ + protected function toDisplayPath($virtualPath) + { + if ($virtualPath === '' || $virtualPath === '/') { + return '/'; + } + + $tokens = explode('/', trim($virtualPath, '/')); + + /** @var DriveFile[] $objects */ + $objects = $this->getObjects($tokens); + $display = ''; + foreach ($tokens as $token) { + if (!isset($objects[$token])) { + throw new FileNotFoundException($virtualPath); + } + if (!empty($display) || $display === '0') { + $display .= '/'; + } + $display .= $this->sanitizeFilename($objects[$token]->getName()); + } + return $display; + } + + protected function toSingleVirtualPath($displayPath, $makeFullVirtualPath = true, $can_throw = true, $createDirsIfNeeded = false, $is_dir = false) + { + try { + $path = $this->toVirtualPath($displayPath, $makeFullVirtualPath, true); + } catch (FileNotFoundException $e) { + if (!$createDirsIfNeeded) { + if ($can_throw) { + throw $e; + } + return false; + } + + $subdir = $is_dir ? $displayPath : self::dirname($displayPath); + if ($subdir === '' || $this->createDir($subdir, new Config(), true) === false) { + if ($can_throw) { + throw $e; + } + return false; + } + + try { + $path = $this->toVirtualPath($displayPath, $makeFullVirtualPath, true); + } catch (FileNotFoundException $e) { + if ($can_throw) { + throw $e; + } + return false; + } + } + return $path; + } + + protected function canRequest($id, $is_full_req) + { + if (!isset($this->requestedIds[$id])) { + return true; + } + if ($is_full_req && $this->requestedIds[$id]['type'] === false) { + return true; + } // we're making a full dir request and previous request was dirs only...allow + if (time() - $this->requestedIds[$id]['time'] > self::FILE_OBJECT_MINIMUM_VALID_TIME) { + return true; + } + return false; // not yet + } + + protected function markRequest($id, $is_full_req) + { + $this->requestedIds[$id] = [ + 'type' => (bool)$is_full_req, + 'time' => time() + ]; + } + + /** + * @param string|string[] $id + * @param bool $reset_all + */ + protected function resetRequest($id, $reset_all = false) + { + if ($reset_all) { + $this->requestedIds = []; + } else { + if (is_array($id)) { + foreach ($id as $i) { + if ($i === $this->root) { + unset($this->requestedIds['']); + } + unset($this->requestedIds[$i]); + } + } else { + if ($id === $this->root) { + unset($this->requestedIds['']); + } + unset($this->requestedIds[$id]); + } + } + } + + protected function sanitizeFilename($filename) + { + if (!empty($this->options['sanitize_chars'])) { + $filename = str_replace( + $this->options['sanitize_chars'], + $this->options['sanitize_replacement_char'], + $filename + ); + } + + return $filename; + } + + public static function dirname($path) + { + // fix for Flysystem bug on Windows + $path = Util::normalizeDirname(dirname($path)); + return str_replace('\\', '/', $path); + } + + protected function applyDefaultParams($params, $cmdName) + { + if (isset($this->optParams[$cmdName]) && is_array($this->optParams[$cmdName])) { + return array_replace($this->optParams[$cmdName], $params); + } else { + return $params; + } + } + + /** + * Enables empty google drive trash + * + * @return void + * + * @see https://developers.google.com/drive/v3/reference/files emptyTrash + * @see \Google_Service_Drive_Resource_Files + */ + public function emptyTrash(array $params = []) + { + $this->refreshToken(); + $this->service->files->emptyTrash($this->applyDefaultParams($params, 'files.emptyTrash')); + } + + /** + * Enables Team Drive support by changing default parameters + * + * @return void + * + * @see https://developers.google.com/drive/v3/reference/files + * @see \Google_Service_Drive_Resource_Files + */ + public function enableTeamDriveSupport() + { + $this->optParams = array_merge_recursive( + array_fill_keys([ + 'files.copy', 'files.create', 'files.delete', + 'files.trash', 'files.get', 'files.list', 'files.update', + 'files.watch' + ], ['supportsTeamDrives' => true]), + $this->optParams + ); + } + + /** + * Selects Team Drive to operate by changing default parameters + * + * @param string $teamDriveId Team Drive id + * @param string $corpora Corpora value for files.list + * @return void + * + * @see https://developers.google.com/drive/v3/reference/files + * @see https://developers.google.com/drive/v3/reference/files/list + * @see \Google_Service_Drive_Resource_Files + */ + public function setTeamDriveId($teamDriveId, $corpora = 'teamDrive') + { + $this->enableTeamDriveSupport(); + $this->optParams = array_merge_recursive($this->optParams, [ + 'files.list' => [ + 'corpora' => $corpora, + 'includeTeamDriveItems' => true, + 'teamDriveId' => $teamDriveId + ] + ]); + + if ($this->root === 'root' || $this->root === null) { + $this->setPathPrefix($teamDriveId); + $this->root = $teamDriveId; + } + } +} diff --git a/app/Services/Google/StreamableUpload.php b/app/Services/Google/StreamableUpload.php new file mode 100644 index 000000000..ec99fb73a --- /dev/null +++ b/app/Services/Google/StreamableUpload.php @@ -0,0 +1,424 @@ +client = $client; + $this->request = $request; + $this->mimeType = $mimeType; + if ($data !== null) { + if (function_exists('\GuzzleHttp\Psr7\stream_for')) { + $this->data = \GuzzleHttp\Psr7\stream_for($data); + } else { + $this->data = \GuzzleHttp\Psr7\Utils::streamFor($data); + } + } else { + $this->data = null; + } + $this->resumable = $resumable; + $this->chunkSize = is_bool($chunkSize) ? 0 : $chunkSize; + $this->progress = 0; + $this->size = '*'; + if ($this->data !== null) { + $size = $this->data->getSize(); + if ($size !== null) { + $this->size = $size; + } + } + + $this->process(); + } + + /** + * Set the size of the file that is being uploaded. + * + * @param int $size file size in bytes + */ + public function setFileSize($size) + { + $this->size = $size; + } + + /** + * Return the progress on the upload + * + * @return int progress in bytes uploaded. + */ + public function getProgress() + { + return $this->progress; + } + + /** + * Send the next part of the file to upload. + * + * @param null|bool|string|StreamInterface $chunk The next set of bytes to send. If stream is provided then chunkSize is ignored. + * If false it will use $this->data set at construct time. + * @return false|mixed + */ + public function nextChunk($chunk = false) + { + $resumeUri = $this->getResumeUri(); + + if ($chunk === null || is_bool($chunk)) { + if ($this->chunkSize < 1) { + throw new \InvalidArgumentException('Invalid chunk size'); + } + if (!$this->data instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid data stream'); + } + $this->data->seek($this->progress, SEEK_SET); + if ($this->data->eof()) { + return true; // finished + } + $chunk = new LimitStream($this->data, $this->chunkSize, $this->data->tell()); + } else { + if (function_exists('\GuzzleHttp\Psr7\stream_for')) { + $chunk = \GuzzleHttp\Psr7\stream_for($chunk); + } else { + $chunk = \GuzzleHttp\Psr7\Utils::streamFor($chunk); + } + } + $size = $chunk->getSize(); + + if ($size === null) { + throw new \InvalidArgumentException('Chunk doesn\'t support getSize'); + } else { + if ($size < 1) { + return true; // finished + } + + $lastBytePos = $this->progress + $size - 1; + $headers = [ + 'content-range' => 'bytes '.$this->progress.'-'.$lastBytePos.'/'.$this->size, + 'content-length' => $size, + 'expect' => '', + ]; + } + + $request = new Request( + 'PUT', + $resumeUri, + $headers, + $chunk + ); + + return $this->makePutRequest($request); + } + + /** + * Return the HTTP result code from the last call made. + * + * @return int code + */ + public function getHttpResultCode() + { + return $this->httpResultCode; + } + + /** + * Sends a PUT-Request to google drive and parses the response, + * setting the appropriate variables from the response() + * + * @param RequestInterface $request the request which will be sent + * @return false|mixed false when the upload is unfinished or the decoded http response + */ + private function makePutRequest(RequestInterface $request) + { + /** @var ResponseInterface $response */ + $response = $this->client->execute($request); + $this->httpResultCode = $response->getStatusCode(); + + if (308 == $this->httpResultCode) { + // Track the amount uploaded. + $range = $response->getHeaderLine('range'); + if ($range) { + $range_array = explode('-', $range); + $this->progress = $range_array[1] + 1; + } + + // Allow for changing upload URLs. + $location = $response->getHeaderLine('location'); + if ($location) { + $this->resumeUri = $location; + } + + // No problems, but upload not complete. + return false; + } + + // return REST::decodeHttpResponse($response, $this->request); + return \Google_Http_REST::decodeHttpResponse($response, $this->request); + } + + /** + * Resume a previously unfinished upload + * + * @param string $resumeUri The resume-URI of the unfinished, resumable upload. + * @return false|mixed + */ + public function resume($resumeUri) + { + $this->resumeUri = $resumeUri; + $headers = [ + 'content-range' => 'bytes */'.$this->size, + 'content-length' => 0, + ]; + $httpRequest = new Request( + 'PUT', + $this->resumeUri, + $headers + ); + + return $this->makePutRequest($httpRequest); + } + + /** + * @return \Psr\Http\Message\RequestInterface $request + * @visible for testing + */ + private function process() + { + $this->transformToUploadUrl(); + $request = $this->request; + + $postBody = ''; + $contentType = false; + + $meta = (string)$request->getBody(); + $meta = is_string($meta) ? json_decode($meta, true) : $meta; + + $uploadType = $this->getUploadType($meta); + $request = $request->withUri( + Uri::withQueryValue($request->getUri(), 'uploadType', $uploadType) + ); + + $mimeType = $this->mimeType ?: $request->getHeaderLine('content-type'); + + if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) { + $contentType = $mimeType; + $postBody = is_string($meta) ? $meta : json_encode($meta); + } else { + if (self::UPLOAD_MEDIA_TYPE == $uploadType) { + $contentType = $mimeType; + $postBody = $this->data; + } else { + if (self::UPLOAD_MULTIPART_TYPE == $uploadType) { + // This is a multipart/related upload. + $boundary = $this->boundary ?: /* @scrutinizer ignore-call */ mt_rand(); + $boundary = str_replace('"', '', $boundary); + $contentType = 'multipart/related; boundary='.$boundary; + $related = "--$boundary\r\n"; + $related .= "Content-Type: application/json; charset=UTF-8\r\n"; + $related .= "\r\n".json_encode($meta)."\r\n"; + $related .= "--$boundary\r\n"; + $related .= "Content-Type: $mimeType\r\n"; + $related .= "Content-Transfer-Encoding: base64\r\n"; + $related .= "\r\n".base64_encode($this->data)."\r\n"; + $related .= "--$boundary--"; + $postBody = $related; + } + } + } + if (function_exists('\GuzzleHttp\Psr7\stream_for')) { + $stream = \GuzzleHttp\Psr7\stream_for($postBody); + } else { + $stream = \GuzzleHttp\Psr7\Utils::streamFor($postBody); + } + + $request = $request->withBody($stream); + + if (isset($contentType) && $contentType) { + $request = $request->withHeader('content-type', $contentType); + } + + return $this->request = $request; + } + + /** + * Valid upload types: + * - resumable (UPLOAD_RESUMABLE_TYPE) + * - media (UPLOAD_MEDIA_TYPE) + * - multipart (UPLOAD_MULTIPART_TYPE) + * + * @param $meta + * @return string + * @visible for testing + */ + public function getUploadType($meta) + { + if ($this->resumable) { + return self::UPLOAD_RESUMABLE_TYPE; + } + + if (false == $meta && $this->data) { + return self::UPLOAD_MEDIA_TYPE; + } + + return self::UPLOAD_MULTIPART_TYPE; + } + + public function getResumeUri() + { + if (null === $this->resumeUri) { + $this->resumeUri = $this->fetchResumeUri(); + } + + return $this->resumeUri; + } + + private function fetchResumeUri() + { + $body = $this->request->getBody(); + if ($body) { + $headers = [ + 'content-type' => 'application/json; charset=UTF-8', + 'content-length' => $body->getSize(), + 'x-upload-content-type' => $this->mimeType, + 'expect' => '', + ]; + if (is_int($this->size)) { + $headers['x-upload-content-length'] = $this->size; + } + + foreach ($headers as $key => $value) { + $this->request = $this->request->withHeader($key, $value); + } + } + + $response = $this->client->execute($this->request, false); + $location = $response->getHeaderLine('location'); + $code = $response->getStatusCode(); + + if (200 == $code && true == $location) { + return $location; + } + + $message = $code; + $body = json_decode((string)$this->request->getBody(), true); + if (isset($body['error']['errors'])) { + $message .= ': '; + foreach ($body['error']['errors'] as $error) { + $message .= $error['domain'].', '.$error['message'].';'; + } + $message = rtrim($message, ';'); + } + + $error = "Failed to start the resumable upload (HTTP {$message})"; + $this->client->getLogger()->error($error); + + throw new GoogleException($error); + } + + private function transformToUploadUrl() + { + $parts = parse_url((string)$this->request->getUri()); + if (!isset($parts['path'])) { + $parts['path'] = ''; + } + $parts['path'] = '/upload'.$parts['path']; + $uri = Uri::fromParts($parts); + $this->request = $this->request->withUri($uri); + } + + public function setChunkSize($chunkSize) + { + $this->chunkSize = $chunkSize; + } + + public function getRequest() + { + return $this->request; + } +} From 6d93f534e9a7f82e4ce3c47d63c7e8e28111e524 Mon Sep 17 00:00:00 2001 From: Gunadi Wirawan Date: Tue, 6 Dec 2022 00:00:28 +0800 Subject: [PATCH 08/21] sales return: update tdd --- .../SalesReturnApprovalByEmailTest.php | 91 +++++++++++++++++-- .../SalesReturn/SalesReturnApprovalTest.php | 65 +++++++++++-- .../SalesReturnCancellationApprovalTest.php | 59 ++++++++++-- .../SalesReturn/SalesReturnHistoryTest.php | 38 +++++++- .../Sales/SalesReturn/SalesReturnSetup.php | 17 ++-- .../Sales/SalesReturn/SalesReturnTest.php | 68 ++++++++++++-- 6 files changed, 302 insertions(+), 36 deletions(-) diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php index f9dee68fe..5d1f55ad7 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php @@ -55,7 +55,18 @@ public function success_create_sales_return() $response = $this->json('POST', self::$path, $data, $this->headers); - $response->assertStatus(201); + $response->assertStatus(201) + ->assertJson([ + "data" => [ + "id" => $response->json('data.id'), + "form" => [ + "id" => $response->json('data.form.id'), + "date" => $response->json('data.form.date'), + "number" => $response->json('data.form.number'), + "notes" => $response->json('data.form.notes'), + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -90,7 +101,18 @@ public function success_approve_sales_return() $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -144,8 +166,21 @@ public function success_approve_by_email_sales_return() ]; $response = $this->json('POST', self::$path . '/approve', $data, $this->headers); - - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + "approval_status" => 1, + ] + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'id' => $salesReturn->form->id, 'number' => $salesReturn->form->number, @@ -189,7 +224,21 @@ public function success_approve_delete_by_email_sales_return() $response = $this->json('POST', self::$path . '/approve', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + "cancellation_status" => 1, + ] + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'number' => $salesReturn->form->number, 'cancellation_status' => 1, @@ -243,7 +292,21 @@ public function success_reject_by_email_sales_return() $response = $this->json('POST', self::$path . '/reject', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + "approval_status" => -1, + ] + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'id' => $salesReturn->form->id, 'number' => $salesReturn->form->number, @@ -283,7 +346,21 @@ public function success_reject_delete_by_email_sales_return() $response = $this->json('POST', self::$path . '/reject', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + "cancellation_status" => -1, + ] + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'number' => $salesReturn->form->number, 'cancellation_status' => -1, diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php index 510479b41..02395ad04 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php @@ -27,7 +27,18 @@ public function success_create_sales_return($isFirstCreate = true) $response = $this->json('POST', self::$path, $data, $this->headers); - $response->assertStatus(201); + $response->assertStatus(201) + ->assertJson([ + "data" => [ + "id" => $response->json('data.id'), + "form" => [ + "id" => $response->json('data.form.id'), + "date" => $response->json('data.form.date'), + "number" => $response->json('data.form.number'), + "notes" => $response->json('data.form.notes'), + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -62,7 +73,19 @@ public function success_approve_sales_return() $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + "approval_status" => 1, + ] + ] + ]); $subTotal = $response->json('data.amount') - $response->json('data.tax'); $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), @@ -124,7 +147,11 @@ public function invalid_reject_sales_return() $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/reject', [], $this->headers); - $response->assertStatus(422); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid." + ]); } /** @test */ @@ -137,7 +164,19 @@ public function success_reject_sales_return() $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/reject', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + "approval_status" => -1, + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -174,6 +213,7 @@ public function success_read_approval_sales_return() $response = $this->json('GET', self::$path . '/approval', $data, $this->headers); $response->assertStatus(200); + $this->assertGreaterThan(0, count($response->json('data'))); } /** @test */ @@ -186,7 +226,15 @@ public function success_send_approval_sales_return() $response = $this->json('POST', self::$path . '/approval/send', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "input" => [ + "ids" => [ + [ "id" => $salesReturn->id ] + ] + ] + ]); + } /** @test */ @@ -207,6 +255,11 @@ public function success_send_multiple_approval_sales_return() $response = $this->json('POST', self::$path . '/approval/send', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "input" => [ + "ids" => $data['ids'] + ] + ]); } } \ No newline at end of file diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php index 6b6eaa9f4..4c603cd5f 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php @@ -21,7 +21,18 @@ public function success_create_sales_return() $response = $this->json('POST', self::$path, $data, $this->headers); - $response->assertStatus(201); + $response->assertStatus(201) + ->assertJson([ + "data" => [ + "id" => $response->json('data.id'), + "form" => [ + "id" => $response->json('data.form.id'), + "date" => $response->json('data.form.date'), + "number" => $response->json('data.form.number'), + "notes" => $response->json('data.form.notes'), + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -75,7 +86,11 @@ public function invalid_state_cancellation_approve_sales_return() $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-approve', [], $this->headers); - $response->assertStatus(422); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "form not in cancellation pending state" + ]); } /** @test */ @@ -87,7 +102,19 @@ public function success_cancellation_approve_sales_return() $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-approve', [], $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + "cancellation_status" => 1, + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'number' => $salesReturn->form->number, 'cancellation_status' => 1, @@ -127,7 +154,11 @@ public function invalid_cancellation_reject_sales_return() $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', [], $this->headers); - $response->assertStatus(422); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid." + ]); } /** @test */ @@ -141,7 +172,11 @@ public function invalid_state_cancellation_reject_sales_return() $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', $data, $this->headers); - $response->assertStatus(422); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "form not in cancellation pending state" + ]); } /** @test */ @@ -154,7 +189,19 @@ public function success_reject_sales_return() $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + "cancellation_status" => -1, + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'number' => $salesReturn->form->number, 'cancellation_status' => -1, diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php index 6f426e2c1..76d0f2be3 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php @@ -22,7 +22,18 @@ public function success_create_sales_return() $response = $this->json('POST', self::$path, $data, $this->headers); - $response->assertStatus(201); + $response->assertStatus(201) + ->assertJson([ + "data" => [ + "id" => $response->json('data.id'), + "form" => [ + "id" => $response->json('data.form.id'), + "date" => $response->json('data.form.date'), + "number" => $response->json('data.form.number'), + "notes" => $response->json('data.form.notes'), + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -42,7 +53,18 @@ public function success_update_sales_return() $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); - $response->assertStatus(201); + $response->assertStatus(201) + ->assertJson([ + "data" => [ + "id" => $response->json('data.id'), + "form" => [ + "id" => $response->json('data.form.id'), + "date" => $response->json('data.form.date'), + "number" => $response->json('data.form.number'), + "notes" => $response->json('data.form.notes'), + ] + ] + ]); $this->assertDatabaseHas('forms', [ 'edited_number' => $response->json('data.form.number') ], 'tenant'); $this->assertDatabaseHas('user_activities', [ 'number' => $response->json('data.form.number'), @@ -71,6 +93,7 @@ public function read_sales_return_histories() $response = $this->json('GET', self::$path . '/' . $salesReturnUpdated->id . '/histories', $data, $this->headers); $response->assertStatus(200); + $this->assertGreaterThan(0, count($response->json('data'))); $this->assertDatabaseHas('user_activities', [ 'number' => $salesReturn->form->edited_number, 'table_id' => $salesReturn->id, @@ -97,7 +120,16 @@ public function success_create_sales_return_history() $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/histories', $data, $this->headers); - $response->assertStatus(201); + $response->assertStatus(201) + ->assertJson([ + "data" => [ + "table_type" => 'SalesReturn', + "table_id" => $salesReturn->id, + "number" => $salesReturn->form->number, + "activity" => 'Printed', + ] + ]); + $this->assertDatabaseHas('user_activities', [ 'number' => $response->json('data.number'), 'table_id' => $response->json('data.table_id'), diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnSetup.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnSetup.php index d3444d3ca..0d929698d 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnSetup.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnSetup.php @@ -51,6 +51,7 @@ public function setUp(): void $this->createCustomerUnitItem(); $this->setUserWarehouse($this->branchDefault); $this->setApprover(); + $_SERVER['HTTP_REFERER'] = 'http://www.example.com/'; } private function setUserWarehouse($branch = null) @@ -130,9 +131,9 @@ private function generateChartOfAccount() $this->coa->save(); } - $arCoaId = get_setting_journal('sales', 'account receivable'); + $arCoaId = SettingJournal::where('feature', 'sales')->where('name', 'account receivable')->first(); if ($arCoaId) { - $arCoa = ChartOfAccount::where('id', $arCoaId)->first(); + $arCoa = ChartOfAccount::where('id', $arCoaId->chart_of_account_id)->first(); $this->arCoa = $arCoa; } else { $type = new ChartOfAccountType; @@ -159,9 +160,9 @@ private function generateChartOfAccount() $setting->save(); } - $salesIncomeId = get_setting_journal('sales', 'sales income'); + $salesIncomeId = SettingJournal::where('feature', 'sales')->where('name', 'sales income')->first(); if ($salesIncomeId) { - $salesIncomeCoa = ChartOfAccount::where('id', $salesIncomeId)->first(); + $salesIncomeCoa = ChartOfAccount::where('id', $salesIncomeId->chart_of_account_id)->first(); $this->salesIncomeCoa = $salesIncomeCoa; } else { $type = new ChartOfAccountType; @@ -188,9 +189,9 @@ private function generateChartOfAccount() $setting->save(); } - $salesCostCoaId = get_setting_journal('sales', 'cost of sales'); + $salesCostCoaId = SettingJournal::where('feature', 'sales')->where('name', 'cost of sales')->first(); if ($salesCostCoaId) { - $salesCostCoa = ChartOfAccount::where('id', $salesCostCoaId)->first(); + $salesCostCoa = ChartOfAccount::where('id', $salesCostCoaId->chart_of_account_id)->first(); $this->salesCostCoa = $salesCostCoa; } else { $type = new ChartOfAccountType; @@ -217,9 +218,9 @@ private function generateChartOfAccount() $setting->save(); } - $taxCoaId = get_setting_journal('sales', 'income tax payable'); + $taxCoaId = SettingJournal::where('feature', 'sales')->where('name', 'income tax payable')->first(); if ($taxCoaId) { - $taxCoa = ChartOfAccount::where('id', $taxCoaId)->first(); + $taxCoa = ChartOfAccount::where('id', $taxCoaId->chart_of_account_id)->first(); $this->taxCoa = $taxCoa; } else { $type = new ChartOfAccountType; diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php index b2598a248..af60b0130 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php @@ -54,7 +54,11 @@ public function invalid_create_sales_return() $response = $this->json('POST', self::$path, $data, $this->headers); - $response->assertStatus(422); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid." + ]); } /** @test */ @@ -66,7 +70,19 @@ public function success_create_sales_return() $response = $this->json('POST', self::$path, $data, $this->headers); - $response->assertStatus(201); + $response->assertStatus(201) + ->assertJson([ + "data" => [ + "id" => $response->json('data.id'), + "form" => [ + "id" => $response->json('data.form.id'), + "date" => $response->json('data.form.date'), + "number" => $response->json('data.form.number'), + "notes" => $response->json('data.form.notes'), + ] + ] + ]); + $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -84,7 +100,19 @@ public function success_approve_sales_return() $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + ] + ] + ]); + $this->assertDatabaseHas('forms', [ 'id' => $response->json('data.form.id'), 'number' => $response->json('data.form.number'), @@ -137,7 +165,18 @@ public function read_sales_return() $response = $this->json('GET', self::$path . '/' . $salesReturn->id, $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJson([ + "data" => [ + "id" => $salesReturn->id, + "form" => [ + "id" => $salesReturn->form->id, + "date" => $salesReturn->form->date, + "number" => $salesReturn->form->number, + "notes" => $salesReturn->form->notes, + ] + ] + ]); } /** @test */ @@ -211,7 +250,11 @@ public function invalid_update_sales_return() $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); - $response->assertStatus(422); + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "The given data was invalid." + ]); } /** @test */ @@ -226,7 +269,19 @@ public function success_update_sales_return() $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); - $response->assertStatus(201); + $response->assertStatus(201) + ->assertJson([ + "data" => [ + "id" => $response->json('data.id'), + "form" => [ + "id" => $response->json('data.form.id'), + "date" => $response->json('data.form.date'), + "number" => $response->json('data.form.number'), + "notes" => $response->json('data.form.notes'), + ] + ] + ]); + $this->assertDatabaseHas('forms', [ 'edited_number' => $response->json('data.form.number') ], 'tenant'); $this->assertDatabaseHas('user_activities', [ 'number' => $response->json('data.form.number'), @@ -282,6 +337,7 @@ public function success_delete_sales_return() $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); $response->assertStatus(204); + $this->assertDatabaseHas('forms', [ 'number' => $salesReturn->form->number, 'request_cancellation_reason' => $data['reason'], From 5d234b63735e22202da585cf28c05b13dee2c5d8 Mon Sep 17 00:00:00 2001 From: Bayu Ramadhan Date: Thu, 8 Dec 2022 20:21:32 +0700 Subject: [PATCH 09/21] purchase request - update test case --- routes/api/purchase.php | 4 +- .../Request/PurchaseRequestApprovalTest.php | 55 ++- .../Request/PurchaseRequestCloseTest.php | 113 ++++--- .../Request/PurchaseRequestPermissionTest.php | 55 +++ .../Purchase/Request/PurchaseRequestSetup.php | 113 ++++++- .../Purchase/Request/PurchaseRequestTest.php | 313 +++++++++++++++++- tests/TestCase.php | 6 +- 7 files changed, 575 insertions(+), 84 deletions(-) create mode 100644 tests/Feature/Http/Purchase/Request/PurchaseRequestPermissionTest.php diff --git a/routes/api/purchase.php b/routes/api/purchase.php index b6561b5e8..8c487f630 100644 --- a/routes/api/purchase.php +++ b/routes/api/purchase.php @@ -11,8 +11,8 @@ Route::post('requests/{id}/cancellation-approve', 'PurchaseRequest\\PurchaseRequestCancellationApprovalController@approve'); Route::post('requests/{id}/cancellation-reject', 'PurchaseRequest\\PurchaseRequestCancellationApprovalController@reject'); Route::post('requests/{id}/close', 'PurchaseRequest\\PurchaseRequestCloseController@close'); - Route::post('requests/{id}/close-approve', 'PurchaseRequest\\PurchaseRequestCloseController@approve'); - Route::post('requests/{id}/close-reject', 'PurchaseRequest\\PurchaseRequestCloseController@reject'); + // Route::post('requests/{id}/close-approve', 'PurchaseRequest\\PurchaseRequestCloseController@approve'); + // Route::post('requests/{id}/close-reject', 'PurchaseRequest\\PurchaseRequestCloseController@reject'); Route::post('requests/send-bulk-request-approval', 'PurchaseRequest\\PurchaseRequestController@sendBulkRequestApproval'); Route::post('requests/approval-with-token/bulk', 'PurchaseRequest\\PurchaseRequestApprovalController@bulkApprovalWithToken'); Route::apiResource('requests', 'PurchaseRequest\\PurchaseRequestController'); diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php index e98ca96e5..08698008c 100644 --- a/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php @@ -25,7 +25,12 @@ public function unauthorized_reject_purchase_request() $this->success_create_purchase_request(); $this->unsetUserRole(); - $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/reject', [], $this->headers); + $data = [ + 'id' => $this->purchase->id, + 'reason' => 'reason' + ]; + + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/reject', $data, $this->headers); $response->assertStatus(422) ->assertJson([ @@ -35,13 +40,31 @@ public function unauthorized_reject_purchase_request() } /** @test */ - public function reject_purchase_request() + public function failed_reject_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $data = [ + 'id' => $this->purchase->id, + ]; + + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/reject', $data, $this->headers); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "The given data was invalid." + ]); + } + + /** @test */ + public function success_reject_purchase_request() { //create purchase request and save to $this->purchase $this->success_create_purchase_request(); /* s: reject test */ $data = [ 'id' => $this->purchase->id, + 'reason' => 'reason' ]; $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/reject', $data, $this->headers); @@ -65,7 +88,7 @@ public function unauthorized_approve_purchase_request() } /** @test */ - public function approve_purchase_request() + public function success_approve_purchase_request() { //create purchase request and save to $this->purchase $this->success_create_purchase_request(); @@ -119,17 +142,20 @@ public function failed_approval_by_email_purchase_request() ]; $response = $this->json('POST', self::$path.'/approval-with-token/bulk', $data, $this->headers); - $response->assertStatus(422); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "Not Authorized" + ]); /* e: bulk approval email fail test */ } /** @test */ - public function success_approval_by_email_purchase_request() + public function success_reject_by_email_purchase_request() { $this->success_request_approval_by_email_purchase_request(); /* s: bulk approval email test */ - $token = Token::take(1)->first(); + $token = Token::where('user_id', $this->user->id)->first(); $data = [ 'token' => $token->token, 'bulk_id' => array($this->purchase->id), @@ -141,4 +167,21 @@ public function success_approval_by_email_purchase_request() /* e: bulk approval email test */ } + /** @test */ + public function success_approve_by_email_purchase_request() + { + $this->success_request_approval_by_email_purchase_request(); + + /* s: bulk approval email test */ + $token = Token::where('user_id', $this->user->id)->first(); + $data = [ + 'token' => $token->token, + 'bulk_id' => array($this->purchase->id), + 'status' => 1 + ]; + + $response = $this->json('POST', self::$path.'/approval-with-token/bulk', $data, $this->headers); + $response->assertStatus(200); + /* e: bulk approval email test */ + } } \ No newline at end of file diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php index f67cda0cb..de00cd1ce 100644 --- a/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php @@ -30,7 +30,7 @@ public function approve_purchase_request() } /** @test */ - public function invalid_state_close_purchase_request() + public function invalid_data_close_purchase_request() { //create purchase request and save to $this->purchase $this->success_create_purchase_request(); @@ -39,7 +39,10 @@ public function invalid_state_close_purchase_request() "id" => $this->purchase->id, ]; $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/close', $data, $this->headers); - $response->assertStatus(500); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "The given data was invalid." + ]); } /** @test */ @@ -57,7 +60,7 @@ public function invalid_condition_close_purchase_request() $response->assertStatus(422) ->assertJson([ "code" => 422, - "message" => "form not approved and not in pending state" + "message" => "Form not approved or not in pending state" ]); } @@ -72,57 +75,65 @@ public function success_close_purchase_request() ]; $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/close', $data, $this->headers); $response->assertStatus(204); - } - - /** @test */ - public function invalid_state_close_approve_purchase_request() - { - //create purchase request and save to $this->purchase - $this->success_create_purchase_request(); - - $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-approve', [], $this->headers); - $response->assertStatus(422); - } - - /** @test */ - public function success_close_approve_purchase_request() - { - $this->success_close_purchase_request(); - - $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-approve', [], $this->headers); - $response->assertStatus(200); - } - - /** @test */ - public function invalid_close_reject_purchase_request() - { - $this->success_close_purchase_request(); - $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-reject', [], $this->headers); - $response->assertStatus(422); - } - - /** @test */ - public function invalid_state_close_reject_purchase_request() - { - //create purchase request and save to $this->purchase - $this->success_create_purchase_request(); - - $data["reason"] = "reject"; - $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-approve', $data, $this->headers); - $response->assertStatus(422); + $this->assertDatabaseHas('forms', [ + 'number' => $this->purchase->form->number, + 'close_status' => true + ], 'tenant'); } - /** @test */ - public function success_reject_purchase_request() - { - $this->success_close_purchase_request(); - - $data['reason'] = $this->faker->text(200); - $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-reject', $data, $this->headers); - - $response->assertStatus(200); - } + // /** @test */ + // public function invalid_state_close_approve_purchase_request() + // { + // //create purchase request and save to $this->purchase + // $this->success_create_purchase_request(); + + // $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-approve', [], $this->headers); + // $response->assertStatus(422)->assertJson([ + // "code" => 422, + // "message" => "Form not approved or not in pending state" + // ]); + // } + + // /** @test */ + // public function success_close_approve_purchase_request() + // { + // $this->success_close_purchase_request(); + + // $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-approve', [], $this->headers); + // $response->assertStatus(200); + // } + + // /** @test */ + // public function invalid_close_reject_purchase_request() + // { + // $this->success_close_purchase_request(); + + // $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-reject', [], $this->headers); + // $response->assertStatus(422); + // } + + // /** @test */ + // public function invalid_state_close_reject_purchase_request() + // { + // //create purchase request and save to $this->purchase + // $this->success_create_purchase_request(); + + // $data["reason"] = "reject"; + // $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-approve', $data, $this->headers); + // $response->assertStatus(422); + // } + + // /** @test */ + // public function success_reject_purchase_request() + // { + // $this->success_close_purchase_request(); + + // $data['reason'] = $this->faker->text(200); + // $response = $this->json('POST', self::$path . '/' . $this->purchase->id . '/close-reject', $data, $this->headers); + + // $response->assertStatus(200); + // } /** @test */ public function success_autoclose_purchase_request() diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestPermissionTest.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestPermissionTest.php new file mode 100644 index 000000000..4e993ae27 --- /dev/null +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestPermissionTest.php @@ -0,0 +1,55 @@ +setupUser(true, false); + + $this->assertFalse($this->tenantUser->hasPermissionTo('menu purchase')); + $this->assertFalse($this->tenantUser->hasPermissionTo('create purchase request')); + $this->assertFalse($this->tenantUser->hasPermissionTo('read purchase request')); + $this->assertFalse($this->tenantUser->hasPermissionTo('update purchase request')); + $this->assertFalse($this->tenantUser->hasPermissionTo('delete purchase request')); + $this->assertFalse($this->tenantUser->hasPermissionTo('approve purchase request')); + } + + /** @test */ + public function check_true_permission_access() + { + $this->setupUser(true, true); + + $this->assertTrue($this->tenantUser->hasPermissionTo('menu purchase')); + $this->assertTrue($this->tenantUser->hasPermissionTo('create purchase request')); + $this->assertTrue($this->tenantUser->hasPermissionTo('read purchase request')); + $this->assertTrue($this->tenantUser->hasPermissionTo('update purchase request')); + $this->assertTrue($this->tenantUser->hasPermissionTo('delete purchase request')); + $this->assertTrue($this->tenantUser->hasPermissionTo('approve purchase request')); + } + + /** @test */ + public function add_permission_access() + { + $this->setupUser(true, false); + + $this->assertFalse($this->tenantUser->hasPermissionTo('approve purchase request')); + + //add permission + $data = [ + "permission_name" => "approve purchase request", + "role_id" => $this->role->id + ]; + + $response = $this->json('PATCH', '/api/v1/master/roles/'.$this->role->id.'/permissions', $data, $this->headers); + $response->assertStatus(200); + + $this->assertTrue($this->tenantUser->hasPermissionTo('approve purchase request')); + } +} \ No newline at end of file diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestSetup.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestSetup.php index 47087bb65..282b9b3fb 100644 --- a/tests/Feature/Http/Purchase/Request/PurchaseRequestSetup.php +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestSetup.php @@ -9,29 +9,54 @@ use App\Model\Auth\Permission; use App\Model\Master\Allocation; use App\Model\Master\User as TenantUser; +use App\Model\Master\Supplier; +use DateTime; +use DateTimeZone; +use Faker\Factory; trait PurchaseRequestSetup { public static $path = '/api/v1/purchase/requests'; - protected $item = null; - protected $allocation = null; - protected $purchase = null; + private $tenantUser; + private $branchDefault; + protected $item; + protected $allocation; + protected $purchase; public function setUp(): void { parent::setUp(); - $this->signIn(); + $this->setupUser(); $this->setProject(); - $this->setPurchaseRequestPermission(); $this->createSampleChartAccountType(); $this->createSampleEmployee(); $this->createSampleItem(); $this->createSampleAllocation(); } + public function setupUser($customRole = false, $setupPermission = true) + { + $this->signIn(); + if($customRole){ + $this->setCustomRole(); + }else{ + $this->setRole(); + } + if($setupPermission){ + $this->setPurchaseRequestPermission(); + } + $this->tenantUser = TenantUser::find($this->user->id); + } + + protected function unsetBranch() + { + foreach ($this->tenantUser->branches as $branch) { + $this->tenantUser->branches()->detach($branch->pivot->branch_id); + } + } + protected function setPurchaseRequestPermission() { - $this->setRole(); Permission::createIfNotExists('menu purchase'); $permission = ['purchase request']; @@ -48,6 +73,18 @@ protected function setPurchaseRequestPermission() $this->role->syncPermissions($permissions); } + protected function setCustomRole() + { + $faker = Factory::create(); + $role = \App\Model\Auth\Role::createIfNotExists($faker->name); + $hasRole = new \App\Model\Auth\ModelHasRole(); + $hasRole->role_id = $role->id; + $hasRole->model_type = 'App\Model\Master\User'; + $hasRole->model_id = $this->user->id; + $hasRole->save(); + $this->role = $role; + } + protected function createSampleItem() { $item = new Item; @@ -93,6 +130,67 @@ private function createDataPurchaseRequest() return $data; } + private function createSupplier() + { + factory(Supplier::class, 1)->create(); + return Supplier::take(1)->first(); + } + + private function createPurchaseOrder($purchaseRequest) + { + $supplier = $this->createSupplier(); + $data = [ + "increment_group" => date('Ym'), + "date" => date('Y-m-d H:m:s'), + 'supplier_id' => $supplier->id, + 'supplier_name' => $supplier->name, + 'purchase_request_id' => $purchaseRequest->id, + "request_approval_to" => $this->user->id, + "tax" => 95000, + "tax_base" => 950000, + "total" => 1045000, + "discount_percent" => 0, + "discount_value" => 0, + "type_of_tax" => "exclude", + "need_down_payment" => 0, + "cash_only" => false, + "notes" => "Test Note", + "items" => [ + [ + "purchase_request_item_id" => $purchaseRequest->items[0]->id, + "item_id" => $this->item->id, + "item_name" => $this->item->name, + "unit" => "PCS", + "converter" => "1.00", + "quantity" => "20", + "discount_percent" => 0, + "discount_value" => 5000, + "price" => 1000000, + "notes" => "notes", + "allocation_id" => $this->allocation->id, + ] + ] + ]; + $response = $this->json('POST', '/api/v1/purchase/orders', $data, $this->headers); + + // save data + $result = json_decode($response->getContent())->data; + var_dump($result); + + return $result; + } + + protected function convertDateTime($date, $format = 'Y-m-d H:i:s') + { + $tz1 = 'Asia/Jakarta'; + $tz2 = 'UTC'; + + $d = new DateTime($date, new DateTimeZone($tz1)); + $d->setTimeZone(new DateTimeZone($tz2)); + + return $d->format($format); + } + protected function unsetUserRole() { ModelHasRole::where('role_id', $this->role->id) @@ -103,8 +201,7 @@ protected function unsetUserRole() protected function setDefaultBranch($state = true) { - $tenantUser = TenantUser::find($this->user->id); - foreach ($tenantUser->branches as $branch) { + foreach ($this->tenantUser->branches as $branch) { $branch->pivot->is_default = $state; $branch->pivot->save(); } diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestTest.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestTest.php index 423311578..203aaab95 100644 --- a/tests/Feature/Http/Purchase/Request/PurchaseRequestTest.php +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestTest.php @@ -1,6 +1,7 @@ createDataPurchaseRequest(); $response = $this->json('POST', self::$path, $data, $this->headers); - // $response->dump(); - $response->assertStatus(422); + $response->assertStatus(422)->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + ]); + } + + /** @test */ + public function failed_default_branch_create_purchase_request() + { + $data = $this->createDataPurchaseRequest(); + $this->setDefaultBranch(false); + + $response = $this->json('POST', self::$path, $data, $this->headers); + $response->assertStatus(422)->assertJson([ + 'code' => 422, + 'message' => 'please set default branch to save this form', + ]); } /** @test */ @@ -40,7 +56,7 @@ public function success_create_purchase_request() // assert database $this->assertDatabaseHas('purchase_requests', [ 'id' => $this->purchase->id, - 'required_date' => date('Y-m-d H:m:s', strtotime($this->purchase->required_date.' -7 hour')) + 'required_date' => $this->convertDateTime($this->purchase->required_date) ], 'tenant'); $this->assertDatabaseHas('purchase_request_items', [ 'id' => $this->purchase->items[0]->id, @@ -62,17 +78,119 @@ public function read_all_purchase_request() //create purchase request and save to $this->purchase $this->success_create_purchase_request(); - $response = $this->json('GET', self::$path.'?join=form,items,item&fields=purchase_request.*&sort_by=-form.number&group_by=form.id&filter_form=notArchived%3Bnull&filter_like=%7B%7D&filter_not_null=form.number&%7B%22form.date%22:%22'.date('Y-m-01').'+00:00:00%22%7D&filter_date_max=%7B%22form.date%22:%22'.date('Y-m-d').'+23:59:59%22%7D&limit=10&includes=form%3Bitems.item&page=1', array(), $this->headers); + $data = [ + 'join' => 'form,items,item', + 'fields' => 'purchase_request.*', + 'sort_by' => '-form.number', + 'group_by' => 'form.id', + 'filter_form' => 'notArchived;null', + 'filter_like' => '{}', + 'filter_date_min' => '{"form.date":"'.date('Y-m-01 00:00:00').'"}', + 'filter_date_max' => '{"form.date":"'.date('Y-m-d 00:00:00').'"}', + 'limit' => 10, + 'includes' => 'form;items.item;', + 'page' => 1, + ]; + + $response = $this->json('GET', self::$path, $data, $this->headers); + // var_dump($response->getContent()); + // $response = $this->json('GET', self::$path.'?join=form,items,item&fields=purchase_request.*&sort_by=-form.number&group_by=form.id&filter_form=notArchived%3Bnull&filter_like=%7B%7D&filter_not_null=form.number&%7B%22form.date%22:%22'.date('Y-m-01').'+00:00:00%22%7D&filter_date_max=%7B%22form.date%22:%22'.date('Y-m-d').'+23:59:59%22%7D&limit=10&includes=form%3Bitems.item&page=1', array(), $this->headers); $response->assertStatus(200); } /** @test */ - public function read_single_purchase_request() + public function read_all_With_filter_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $data = [ + 'join' => 'form,items,item', + 'fields' => 'purchase_request.*', + 'sort_by' => '-form.number', + 'group_by' => 'form.id', + 'filter_form' => 'notArchived;null', + 'filter_like' => '{}', + 'filter_date_min' => '{"form.date":"'.date('Y-m-15 00:00:00').'"}', + 'filter_date_max' => '{"form.date":"'.date('Y-m-16 00:00:00').'"}', + 'limit' => 10, + 'includes' => 'form;items.item;', + 'page' => 1, + ]; + + $response = $this->json('GET', self::$path, $data, $this->headers); + + // $response = $this->json('GET', self::$path.'?join=form,items,item&fields=purchase_request.*&sort_by=-form.number&group_by=form.id&filter_form=notArchived%3BapprovalPending&filter_like=%7B%7D&filter_not_null=form.number&%7B%22form.date%22:%22'.date('Y-m-15').'+00:00:00%22%7D&filter_date_max=%7B%22form.date%22:%22'.date('Y-m-d').'+23:59:59%22%7D&limit=10&includes=form%3Bitems.item&page=1', array(), $this->headers); + $response->assertStatus(200); + } + + /** @test */ + public function read_all_With_search_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $data = [ + 'join' => 'form,items,item', + 'fields' => 'purchase_request.*', + 'sort_by' => '-form.number', + 'group_by' => 'form.id', + 'filter_form' => 'notArchived;null', + 'filter_like' => '{"form.number":"'.$this->purchase->form->number.'"}', + 'filter_date_min' => '{"form.date":"'.date('Y-m-15 00:00:00').'"}', + 'filter_date_max' => '{"form.date":"'.date('Y-m-16 00:00:00').'"}', + 'limit' => 10, + 'includes' => 'form;items.item;', + 'page' => 1, + ]; + + $response = $this->json('GET', self::$path, $data, $this->headers); + // $response = $this->json('GET', self::$path.'?join=form,items,item&fields=purchase_request.*&sort_by=-form.number&group_by=form.id&filter_form=notArchived%3BapprovalPending&filter_like=%7B%22form.number%22:%22'.$this->purchase->form->number.'%22,%22item.code%22:%22'.$this->purchase->form->number.'%22,%22item.name%22:%22'.$this->purchase->form->number.'%22,%22purchase_request_item.notes%22:%22'.$this->purchase->form->number.'%22,%22purchase_request_item.quantity%22:%22'.$this->purchase->form->number.'%22%7D&filter_not_null=form.number&%7B%22form.date%22:%22'.date('Y-m-01').'+00:00:00%22%7D&filter_date_max=%7B%22form.date%22:%22'.date('Y-m-d').'+23:59:59%22%7D&limit=10&includes=form%3Bitems.item&page=1', array(), $this->headers); + $response->assertStatus(200); + } + + /** @test */ + public function failed_not_same_branch_read_single_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + $this->unsetBranch(); + + $response = $this->json('GET', self::$path.'/'.$this->purchase->id.'?includes=items.item;items.allocation;form.requestApprovalTo;form.branch&with_archives=true&with_origin=true', array(), $this->headers); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "Unauthorized" + ]); + } + + /** @test */ + public function failed_access_read_single_purchase_request() { //create purchase request and save to $this->purchase $this->success_create_purchase_request(); - $response = $this->json('GET', self::$path.'/'.$this->purchase->id.'?includes=items.item;items.allocation;form.requestApprovalTo;form.branch', array(), $this->headers); + // toggle permission + $data = [ + "permission_name" => "read purchase request", + "role_id" => $this->role->id + ]; + $response = $this->json('PATCH', '/api/v1/master/roles/'.$this->role->id.'/permissions', $data, $this->headers); + $this->assertFalse($this->tenantUser->hasPermissionTo('read purchase request')); + + $response = $this->json('GET', self::$path.'/'.$this->purchase->id.'?includes=items.item;items.allocation;form.requestApprovalTo;form.branch&with_archives=true&with_origin=true', array(), $this->headers); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "Unauthorized" + ]); + } + + /** @test */ + public function success_read_single_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $response = $this->json('GET', self::$path.'/'.$this->purchase->id.'?includes=items.item;items.allocation;form.requestApprovalTo;form.branch&with_archives=true&with_origin=true', array(), $this->headers); $response->assertStatus(200); } @@ -94,7 +212,30 @@ public function failed_update_purchase_request() $response = $this->json('PATCH', self::$path.'/'.$this->purchase->id, $data, $this->headers); // $response->dump(); - $response->assertStatus(422); + $response->assertStatus(422)->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + ]); + } + + /** @test */ + public function failed_update_purchase_request_linked_puchase_order() + { + // $this->expectOutputString(''); + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + $data = $this->createDataPurchaseRequest(); + $data['id'] = $this->purchase->id; + $data['required_date'] = date('Y-m-30 H:m:s'); + + // link to purchase order + $this-> createPurchaseOrder($this->purchase); + + $response = $this->json('PATCH', self::$path.'/'.$this->purchase->id, $data, $this->headers); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "Cannot edit form because referenced by purchase order" + ]); } /** @test */ @@ -130,6 +271,27 @@ public function success_update_purchase_request() ], 'tenant'); } + /** @test */ + public function success_update_purchase_request_with_different_user() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + // login with different user + $this->setupUser(); + + $data = $this->createDataPurchaseRequest(); + $data['id'] = $this->purchase->id; + $data['required_date'] = date('Y-m-30 H:m:s'); + + $response = $this->json('PATCH', self::$path.'/'.$this->purchase->id, $data, $this->headers); + $response->assertStatus(201); + + $this->assertDatabaseHas('forms', [ + 'created_by' => $this->user->id, + ], 'tenant'); + } + /** @test */ public function failed_delete_purchase_request() { @@ -137,7 +299,29 @@ public function failed_delete_purchase_request() $this->success_create_purchase_request(); $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, [], $this->headers); - $response->assertStatus(422); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "The given data was invalid." + ]); + } + + /** @test */ + public function failed_password_delete_purchase_request() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + + $data = [ + 'id' => $this->purchase->id, + 'tenant_url' => 'http://dev.localhost:8080', + 'password' => 'wrongPassword', + 'reason' => 'Reason' + ]; + $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, $data, $this->headers); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "Unauthorized" + ]); } /** @test */ @@ -149,10 +333,36 @@ public function failed_default_branch_delete_purchase_request() $data = [ 'id' => $this->purchase->id, + 'tenant_url' => 'http://dev.localhost:8080', + 'password' => $this->userPassword, 'reason' => 'Reason' ]; $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, $data, $this->headers); - $response->assertStatus(422); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "Please set as default branch" + ]); + } + + /** @test */ + public function failed_delete_purchase_request_no_access() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + $this->role->revokePermissionTo("delete purchase request"); + + $data = [ + 'id' => $this->purchase->id, + 'tenant_url' => 'http://dev.localhost:8080', + 'password' => $this->userPassword, + 'reason' => 'Reason' + ]; + $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, $data, $this->headers); + // var_dump($response->getContent()); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "Unauthorized" + ]); } /** @test */ @@ -160,14 +370,72 @@ public function success_delete_purchase_request() { //create purchase request and save to $this->purchase $this->success_create_purchase_request(); - /* s: request cancellation test */ + $data = [ 'id' => $this->purchase->id, + 'tenant_url' => 'http://dev.localhost:8080', + 'password' => $this->userPassword, 'reason' => 'Reason' ]; $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, $data, $this->headers); $response->assertStatus(204); - /* e: request cancellation test */ + + $this->assertDatabaseHas('forms', [ + 'number' => $this->purchase->form->number, + 'cancellation_status' => 1, + ], 'tenant'); + } + + /** @test */ + public function failed_delete_purchase_request_with_other_user_no_access() + { + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + $user = $this->user; + $this->role->revokePermissionTo("delete purchase request"); + // login with different user + $this->setupUser(true); + $this->role->revokePermissionTo("delete purchase request"); + + $data = [ + 'id' => $this->purchase->id, + 'tenant_url' => 'http://dev.localhost:8080', + 'request_cancellation_to' => $user->id, + 'reason' => 'Reason' + ]; + $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, $data, $this->headers); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "Unauthorized" + ]); + } + + /** @test */ + public function success_delete_purchase_request_with_other_user() + { + // $this->expectOutputString(""); + //create purchase request and save to $this->purchase + $this->success_create_purchase_request(); + $user = $this->user; + // login with different user + $this->setupUser(true); + $this->role->revokePermissionTo("delete purchase request"); + + $data = [ + 'id' => $this->purchase->id, + 'tenant_url' => 'http://dev.localhost:8080', + 'request_cancellation_to' => $user->id, + 'reason' => 'Reason' + ]; + $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, $data, $this->headers); + // var_dump($response->getContent()); + $response->assertStatus(204); + + $this->assertDatabaseHas('forms', [ + 'number' => $this->purchase->form->number, + 'request_cancellation_to' => $user->id, + 'cancellation_status' => 0, + ], 'tenant'); } /** @test */ @@ -175,28 +443,41 @@ public function success_approve_delete_purchase_request() { $this->success_delete_purchase_request(); - /* s: cancellation approve test */ $data = [ 'id' => $this->purchase->id ]; $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/cancellation-approve', $data, $this->headers); $response->assertStatus(200); - /* e: cancellation approve test */ } /** @test */ - public function success_reject_delete_purchase_request() + public function failed_reject_delete_purchase_request() { $this->success_delete_purchase_request(); - /* s: cancellation approve test */ $data = [ 'id' => $this->purchase->id ]; + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/cancellation-reject', $data, $this->headers); + $response->assertStatus(422)->assertJson([ + "code" => 422, + "message" => "The given data was invalid." + ]); + } + + /** @test */ + public function success_reject_delete_purchase_request() + { + $this->success_delete_purchase_request(); + + $data = [ + 'id' => $this->purchase->id, + 'reason' => 'reason' + ]; + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/cancellation-reject', $data, $this->headers); $response->assertStatus(200); - /* e: cancellation approve test */ } } \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 959d6dc40..c2e358847 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -39,6 +39,7 @@ abstract class TestCase extends BaseTestCase * @var null|User */ protected $user; + protected $userPassword; protected $account = null; protected $employee = null; @@ -76,7 +77,10 @@ protected function tearDown(): void protected function signIn() { - $this->user = factory(User::class)->create(); + $this->userPassword = 'password'; + $this->user = factory(User::class)->create([ + 'password' => bcrypt($this->userPassword), + ]); $this->actingAs($this->user, 'api'); From 9ff509a60d1df6dcf1f6a77a3954b36030ec7902 Mon Sep 17 00:00:00 2001 From: Gunadi Wirawan Date: Thu, 8 Dec 2022 22:05:48 +0800 Subject: [PATCH 10/21] sales return: update tdd --- .../SalesReturn/SalesReturnApprovalTest.php | 20 +++- .../SalesReturn/SalesReturnHistoryTest.php | 17 ++- .../Sales/SalesReturn/SalesReturnTest.php | 104 +++++++++++++++++- 3 files changed, 135 insertions(+), 6 deletions(-) diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php index 02395ad04..c5630c4e4 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php @@ -212,7 +212,25 @@ public function success_read_approval_sales_return() $response = $this->json('GET', self::$path . '/approval', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJsonStructure([ + "data" => [ + [ + "id", + "last_request_date", + "items" => [ + [ + "item_name", + "quantity", + ] + ], + "form" => [ + "number", + "date", + ] + ] + ] + ]); $this->assertGreaterThan(0, count($response->json('data'))); } diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php index 76d0f2be3..64be38626 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php @@ -92,7 +92,22 @@ public function read_sales_return_histories() $response = $this->json('GET', self::$path . '/' . $salesReturnUpdated->id . '/histories', $data, $this->headers); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJsonStructure([ + "data" => [ + [ + "id", + "table_type", + "table_id", + "number", + "date", + "user_id", + "activity", + "formable_id", + "user", + ] + ] + ]); $this->assertGreaterThan(0, count($response->json('data'))); $this->assertDatabaseHas('user_activities', [ 'number' => $salesReturn->form->edited_number, diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php index af60b0130..e5b199085 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php @@ -89,6 +89,19 @@ public function success_create_sales_return() 'approval_status' => 0, 'done' => 0, ], 'tenant'); + + $this->assertDatabaseHas('sales_returns', [ + 'id' => $response->json('data.id'), + 'tax' => $response->json('data.tax'), + 'customer_id' => $response->json('data.customer_id'), + 'amount' => $response->json('data.amount'), + ], 'tenant'); + + $this->assertDatabaseHas('sales_return_items', [ + 'sales_return_id' => $response->json('data.id'), + 'item_id' => $response->json('data.items.0.item_id'), + 'quantity' => $response->json('data.items.0.quantity'), + ], 'tenant'); } /** @test */ @@ -109,6 +122,7 @@ public function success_approve_sales_return() "date" => $salesReturn->form->date, "number" => $salesReturn->form->number, "notes" => $salesReturn->form->notes, + "approval_status" => 1, ] ] ]); @@ -136,16 +150,53 @@ public function read_all_sales_return() 'fields' => 'sales_return.*', 'sort_by' => '-form.number', 'group_by' => 'form.id', - 'filter_form' => 'notArchived;null', + 'filter_form' => 'notArchived', 'filter_like' => '{}', 'limit' => 10, - 'includes' => 'form;customer;items.item;items.allocation', + 'includes' => 'customer;warehouse;items.item;items.allocation;salesInvoice.form;form.createdBy;form.requestApprovalTo;form.branch', 'page' => 1 ]; $response = $this->json('GET', self::$path, $data, $this->headers); - - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJsonStructure([ + "data" => [ + [ + "id", + "tax", + "amount", + "warehouse" => [ + "id", + "name", + ], + "sales_invoice" => [ + "form" => [ + "number", + ] + ], + "form" => [ + "id", + "date", + "number", + "notes", + ], + "items" => [ + [ + "id", + "item_id", + "item_name", + "quantity", + "quantity_sales", + "price", + "discount_value", + "unit", + "converter", + "allocation" + ] + ] + ] + ] + ]); $this->assertGreaterThan(0, count($response->json('data'))); } @@ -169,11 +220,36 @@ public function read_sales_return() ->assertJson([ "data" => [ "id" => $salesReturn->id, + "tax" => $salesReturn->tax, + "amount" => $salesReturn->amount, + "warehouse" => [ + "id" => $salesReturn->warehouse->id, + "name" => $salesReturn->warehouse->name, + ], + "sales_invoice" => [ + "form" => [ + "number" => $salesReturn->salesInvoice->form->number, + ] + ], "form" => [ "id" => $salesReturn->form->id, "date" => $salesReturn->form->date, "number" => $salesReturn->form->number, "notes" => $salesReturn->form->notes, + ], + "items" => [ + [ + "id" => $salesReturn->items[0]->id, + "item_id" => $salesReturn->items[0]->item->id, + "item_name" => $salesReturn->items[0]->item->name, + "quantity" => $salesReturn->items[0]->quantity, + "quantity_sales" => $salesReturn->items[0]->quantity_sales, + "price" => $salesReturn->items[0]->price, + "discount_value" => $salesReturn->items[0]->discount_value, + "unit" => $salesReturn->items[0]->unit, + "converter" => $salesReturn->items[0]->converter, + "allocation" => null + ] ] ] ]); @@ -289,6 +365,26 @@ public function success_update_sales_return() 'table_type' => 'SalesReturn', 'activity' => 'Update - 1' ], 'tenant'); + + $this->assertDatabaseHas('forms', [ + 'id' => $response->json('data.form.id'), + 'number' => $response->json('data.form.number'), + 'approval_status' => 0, + 'done' => 0, + ], 'tenant'); + + $this->assertDatabaseHas('sales_returns', [ + 'id' => $response->json('data.id'), + 'tax' => $response->json('data.tax'), + 'customer_id' => $response->json('data.customer_id'), + 'amount' => $response->json('data.amount'), + ], 'tenant'); + + $this->assertDatabaseHas('sales_return_items', [ + 'sales_return_id' => $response->json('data.id'), + 'item_id' => $response->json('data.items.0.item_id'), + 'quantity' => $response->json('data.items.0.quantity'), + ], 'tenant'); } /** @test */ From 50147bfe269c4103f5b3da7e70f746e77833a308 Mon Sep 17 00:00:00 2001 From: Bayu Ramadhan Date: Thu, 8 Dec 2022 21:58:03 +0700 Subject: [PATCH 11/21] purchase request - update test case --- .../Request/PurchaseRequestApprovalTest.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php index 08698008c..2a542e00c 100644 --- a/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php @@ -184,4 +184,26 @@ public function success_approve_by_email_purchase_request() $response->assertStatus(200); /* e: bulk approval email test */ } + + /** @test */ + public function failed_approve_by_email_purchase_request_not_default_branch() + { + $this->success_request_approval_by_email_purchase_request(); + $this->setDefaultBranch(false); + + /* s: bulk approval email test */ + $token = Token::where('user_id', $this->user->id)->first(); + $data = [ + 'token' => $token->token, + 'bulk_id' => array($this->purchase->id), + 'status' => 1 + ]; + + $response = $this->json('POST', self::$path.'/approval-with-token/bulk', $data, $this->headers); + $response->assertStatus(422)->assertJson([ + 'code' => 422, + 'message' => 'Please set as default branch', + ]); + /* e: bulk approval email test */ + } } \ No newline at end of file From 56aa45a64eccdf23dbdb840dbb7115f158fd6c9d Mon Sep 17 00:00:00 2001 From: Gunadi Wirawan Date: Fri, 16 Dec 2022 01:26:24 +0800 Subject: [PATCH 12/21] update sales return --- app/Exceptions/ApiExceptionHandler.php | 8 + .../SalesReturnApprovalByEmailController.php | 23 + .../SalesReturnApprovalController.php | 49 +- ...esReturnCancellationApprovalController.php | 10 +- .../SalesReturn/SalesReturnController.php | 5 +- .../SalesReturn/DeleteSalesReturnRequest.php | 33 + .../SalesReturn/RejectSalesReturnRequest.php | 33 + .../SalesReturn/StoreSalesReturnRequest.php | 8 +- .../SalesReturn/UpdateSalesReturnRequest.php | 9 +- app/Http/Requests/ValidationRule.php | 1 + app/Model/Sales/SalesInvoice/SalesInvoice.php | 17 + .../SalesInvoice/SalesInvoiceReference.php | 49 + app/Model/Sales/SalesReturn/SalesReturn.php | 154 +- ...064705_create_sales_invoice_references.php | 35 + .../SalesReturnApprovalByEmailTest.php | 419 ++-- .../SalesReturn/SalesReturnApprovalTest.php | 688 ++++--- .../SalesReturnCancellationApprovalTest.php | 528 +++-- .../SalesReturn/SalesReturnHistoryTest.php | 149 +- .../Sales/SalesReturn/SalesReturnSetup.php | 63 +- .../Sales/SalesReturn/SalesReturnTest.php | 1736 +++++++++++++---- 20 files changed, 2900 insertions(+), 1117 deletions(-) create mode 100644 app/Http/Requests/Sales/SalesReturn/SalesReturn/DeleteSalesReturnRequest.php create mode 100644 app/Http/Requests/Sales/SalesReturn/SalesReturn/RejectSalesReturnRequest.php create mode 100644 app/Model/Sales/SalesInvoice/SalesInvoiceReference.php create mode 100644 database/migrations/tenant/2022_12_15_064705_create_sales_invoice_references.php diff --git a/app/Exceptions/ApiExceptionHandler.php b/app/Exceptions/ApiExceptionHandler.php index 6c16c35c4..2be7e72e9 100644 --- a/app/Exceptions/ApiExceptionHandler.php +++ b/app/Exceptions/ApiExceptionHandler.php @@ -94,6 +94,14 @@ public function apiExceptions($request, Throwable $exception) ], 400); } + /* Handle if contain constraint violation but not instanceof QueryExcepetion */ + if (strpos($exception->getMessage(), 'Integrity constraint violation') !== false) { + return response()->json([ + 'code' => 400, + 'message' => 'Duplicate data entry', + ], 400); + } + /* Handle server error or library error */ if ($exception->getCode() >= 500 || ! $exception->getCode()) { return response()->json([ diff --git a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php index caf2028e8..efbfe8074 100644 --- a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php +++ b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php @@ -11,6 +11,8 @@ use App\Model\UserActivity; use App\Model\Sales\SalesReturn\SalesReturn; +use Exception; +use App\Model\Sales\SalesInvoice\SalesInvoiceReference; class SalesReturnApprovalByEmailController extends Controller { @@ -37,12 +39,31 @@ public function approve(Request $request) $result = DB::connection('tenant')->transaction(function () use ($request) { $salesReturns = SalesReturn::whereIn('id', $request->ids)->get(); + + foreach ($salesReturns as $salesReturn) { + try { + if ($salesReturn->form->approval_status === 1) { + throw new Exception('form '.$salesReturn->form->number.' already approved', 422); + } + } catch (\Throwable $th) { + return response_error($th); + } + } foreach ($salesReturns as $salesReturn) { $form = $salesReturn->form; // approve cancellation form if ($form->cancellation_status === 0 && is_null($form->close_status)) { + if($form->approval_status === 1) { + SalesReturn::updateInvoiceQuantity($salesReturn, 'revert'); + Inventory::where('form_id', $salesReturn->form->id)->delete(); + Journal::where('form_id', $salesReturn->form->id)->orWhere('form_id_reference', $salesReturn->form->id)->delete(); + SalesInvoiceReference::where('sales_invoice_id', $salesReturn->sales_invoice_id) + ->where('referenceable_id', $salesReturn->id) + ->where('referenceable_type', 'SalesReturn')->delete(); + } + $form->cancellation_approval_by = $request->approver_id; $form->cancellation_approval_at = now(); $form->cancellation_status = 1; @@ -68,6 +89,7 @@ public function approve(Request $request) SalesReturn::updateJournal($salesReturn); SalesReturn::updateInventory($salesReturn->form, $salesReturn); SalesReturn::updateInvoiceQuantity($salesReturn, 'update'); + SalesReturn::updateSalesInvoiceReference($salesReturn); $form->fireEventApprovedByEmail(); @@ -88,6 +110,7 @@ public function approve(Request $request) */ public function reject(Request $request) { + $validated = $request->validate([ 'reason' => 'required|max:255' ]); $this->request = $request; $result = DB::connection('tenant')->transaction(function () use ($request) { diff --git a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalController.php b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalController.php index bef0cfab0..c7cf13b18 100644 --- a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalController.php +++ b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalController.php @@ -13,7 +13,7 @@ use App\Model\Form; use App\Model\UserActivity; use App\Model\Sales\SalesReturn\SalesReturn; - +use Exception; use App\Mail\Sales\SalesReturnApprovalRequest; class SalesReturnApprovalController extends Controller @@ -68,27 +68,38 @@ public function index(Request $request) */ public function approve(Request $request, $id) { - $result = DB::connection('tenant')->transaction(function () use ($id) { - $salesReturn = SalesReturn::findOrFail($id); - $salesReturn->checkQuantity($salesReturn->items); - - $form = $salesReturn->form; - $form->approval_by = auth()->user()->id; - $form->approval_at = now(); - $form->approval_status = 1; - $form->save(); - - SalesReturn::updateJournal($salesReturn); - SalesReturn::updateInventory($salesReturn->form, $salesReturn); - SalesReturn::updateInvoiceQuantity($salesReturn, 'update'); + try { + $salesReturn = SalesReturn::findOrFail($id); + $salesReturn->checkQuantity($salesReturn->items); + + $form = $salesReturn->form; + + if ($form->approval_status === 1) { + throw new Exception('form already approved', 422); + } + + $form->approval_by = auth()->user()->id; + $form->approval_at = now(); + $form->approval_status = 1; + $form->save(); + SalesReturn::updateJournal($salesReturn); + SalesReturn::updateInventory($salesReturn->form, $salesReturn); + SalesReturn::updateInvoiceQuantity($salesReturn, 'update'); + SalesReturn::updateSalesInvoiceReference($salesReturn); + + + $form->fireEventApproved(); + + } catch (\Throwable $th) { + return response_error($th); + } - $form->fireEventApproved(); - + $salesReturn = SalesReturn::findOrFail($id); + $salesReturn->load('form'); return new ApiResource($salesReturn); }); - return $result; } @@ -100,7 +111,7 @@ public function approve(Request $request, $id) */ public function reject(Request $request, $id) { - $validated = $request->validate([ 'reason' => 'required' ]); + $validated = $request->validate([ 'reason' => 'required|max:255' ]); $result = DB::connection('tenant')->transaction(function () use ($request, $validated, $id) { $salesReturn = SalesReturn::findOrFail($id); @@ -112,6 +123,8 @@ public function reject(Request $request, $id) $salesReturn->form->fireEventRejected(); + $salesReturn = SalesReturn::findOrFail($id); + $salesReturn->load('form'); return new ApiResource($salesReturn); }); diff --git a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnCancellationApprovalController.php b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnCancellationApprovalController.php index 8f1cb10ff..36438d5f1 100644 --- a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnCancellationApprovalController.php +++ b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnCancellationApprovalController.php @@ -10,6 +10,7 @@ use App\Model\Sales\SalesReturn\SalesReturn; use App\Model\Accounting\Journal; use App\Model\Inventory\Inventory; +use App\Model\Sales\SalesInvoice\SalesInvoiceReference; class SalesReturnCancellationApprovalController extends Controller { @@ -32,6 +33,9 @@ public function approve(Request $request, $id) SalesReturn::updateInvoiceQuantity($salesReturn, 'revert'); Inventory::where('form_id', $salesReturn->form->id)->delete(); Journal::where('form_id', $salesReturn->form->id)->orWhere('form_id_reference', $salesReturn->form->id)->delete(); + SalesInvoiceReference::where('sales_invoice_id', $salesReturn->sales_invoice_id) + ->where('referenceable_id', $salesReturn->id) + ->where('referenceable_type', 'SalesReturn')->delete(); } $salesReturn->form->cancellation_approval_by = auth()->user()->id; @@ -39,6 +43,8 @@ public function approve(Request $request, $id) $salesReturn->form->cancellation_status = 1; $salesReturn->form->save(); + $salesReturn = SalesReturn::findOrFail($salesReturn->id); + $salesReturn->load('form'); $salesReturn->form->fireEventCancelApproved(); } catch (\Throwable $th) { return response_error($th); @@ -57,7 +63,7 @@ public function approve(Request $request, $id) */ public function reject(Request $request, $id) { - $request->validate([ 'reason' => 'required ']); + $request->validate([ 'reason' => 'required|max:255']); $salesReturn = SalesReturn::findOrFail($id); @@ -74,6 +80,8 @@ public function reject(Request $request, $id) $salesReturn->form->cancellation_status = -1; $salesReturn->form->save(); + $salesReturn = SalesReturn::findOrFail($salesReturn->id); + $salesReturn->load('form'); $salesReturn->form->fireEventCancelRejected(); } catch (\Throwable $th) { return response_error($th); diff --git a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnController.php b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnController.php index 235e17ac8..c27fd18dd 100644 --- a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnController.php +++ b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnController.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\DB; use App\Http\Requests\Sales\SalesReturn\SalesReturn\StoreSalesReturnRequest; use App\Http\Requests\Sales\SalesReturn\SalesReturn\UpdateSalesReturnRequest; +use App\Http\Requests\Sales\SalesReturn\SalesReturn\DeleteSalesReturnRequest; use Exception; class SalesReturnController extends Controller @@ -45,7 +46,6 @@ public function store(StoreSalesReturnRequest $request) $salesReturn = SalesReturn::create($request->all()); $salesReturn ->load('form') - ->load('customer') ->load('items'); return new ApiResource($salesReturn); @@ -125,7 +125,10 @@ public function destroy(Request $request, $id) $request->validate([ 'reason' => 'required']); $salesReturn->requestCancel($request); + SalesReturn::sendApproval($salesReturn); return response()->json([], 204); + + } } diff --git a/app/Http/Requests/Sales/SalesReturn/SalesReturn/DeleteSalesReturnRequest.php b/app/Http/Requests/Sales/SalesReturn/SalesReturn/DeleteSalesReturnRequest.php new file mode 100644 index 000000000..a861e3f15 --- /dev/null +++ b/app/Http/Requests/Sales/SalesReturn/SalesReturn/DeleteSalesReturnRequest.php @@ -0,0 +1,33 @@ + 'required|max:255', + ]; + + return array_merge($deleteRule); + } +} diff --git a/app/Http/Requests/Sales/SalesReturn/SalesReturn/RejectSalesReturnRequest.php b/app/Http/Requests/Sales/SalesReturn/SalesReturn/RejectSalesReturnRequest.php new file mode 100644 index 000000000..596c526e5 --- /dev/null +++ b/app/Http/Requests/Sales/SalesReturn/SalesReturn/RejectSalesReturnRequest.php @@ -0,0 +1,33 @@ + 'required|max:255', + ]; + + return array_merge($rejectRule); + } +} diff --git a/app/Http/Requests/Sales/SalesReturn/SalesReturn/StoreSalesReturnRequest.php b/app/Http/Requests/Sales/SalesReturn/SalesReturn/StoreSalesReturnRequest.php index 7e96c0fa9..25166c851 100644 --- a/app/Http/Requests/Sales/SalesReturn/SalesReturn/StoreSalesReturnRequest.php +++ b/app/Http/Requests/Sales/SalesReturn/SalesReturn/StoreSalesReturnRequest.php @@ -28,16 +28,22 @@ public function rules() $rulesSalesReturn = [ 'sales_invoice_id' => ValidationRule::foreignKey('sales_invoices'), - 'items' => 'required|array', + 'sub_total' => 'required|numeric|min:0', + 'tax_base' => 'required|numeric|min:0', + 'type_of_tax' => ValidationRule::typeOfTax(), + 'tax' => 'required|numeric|min:0', + 'amount' => 'required|numeric|min:0', ]; $rulesSalesReturnItems = [ 'items.*.sales_invoice_item_id' => ValidationRule::foreignKey('sales_invoice_items'), + 'items.*.item_name' => 'required|string', 'items.*.quantity' => ValidationRule::quantity(), 'items.*.quantity_sales' => ValidationRule::quantity(), 'items.*.unit' => ValidationRule::unit(), 'items.*.converter' => ValidationRule::converter(), + 'items.*.total' => 'required|numeric|min:0', ]; return array_merge($rulesForm, $rulesSalesReturn, $rulesSalesReturnItems); diff --git a/app/Http/Requests/Sales/SalesReturn/SalesReturn/UpdateSalesReturnRequest.php b/app/Http/Requests/Sales/SalesReturn/SalesReturn/UpdateSalesReturnRequest.php index 151e523e0..f719964a4 100644 --- a/app/Http/Requests/Sales/SalesReturn/SalesReturn/UpdateSalesReturnRequest.php +++ b/app/Http/Requests/Sales/SalesReturn/SalesReturn/UpdateSalesReturnRequest.php @@ -32,17 +32,22 @@ public function rules() $rulesSalesReturn = [ 'sales_invoice_id' => ValidationRule::foreignKey('sales_invoices'), - 'warehouse_id' => ValidationRule::foreignKeyNullable('warehouses'), - 'items' => 'required|array', + 'sub_total' => 'required|numeric|min:0', + 'tax_base' => 'required|numeric|min:0', + 'type_of_tax' => ValidationRule::typeOfTax(), + 'tax' => 'required|numeric|min:0', + 'amount' => 'required|numeric|min:0', ]; $rulesSalesReturnItems = [ 'items.*.sales_invoice_item_id' => ValidationRule::foreignKey('sales_invoice_items'), + 'items.*.item_name' => 'required|string', 'items.*.quantity' => ValidationRule::quantity(), 'items.*.quantity_sales' => ValidationRule::quantity(), 'items.*.unit' => ValidationRule::unit(), 'items.*.converter' => ValidationRule::converter(), + 'items.*.total' => 'required|numeric|min:0', ]; return array_merge($rulesForm, $rulesSalesReturn, $rulesSalesReturnItems); diff --git a/app/Http/Requests/ValidationRule.php b/app/Http/Requests/ValidationRule.php index f9cc66a37..6741b8663 100644 --- a/app/Http/Requests/ValidationRule.php +++ b/app/Http/Requests/ValidationRule.php @@ -67,6 +67,7 @@ public static function form() 'date' => 'required|date', 'number'=> 'nullable|string', 'increment_group' => 'required|integer', + 'notes' => 'nullable|max:255', ]; } diff --git a/app/Model/Sales/SalesInvoice/SalesInvoice.php b/app/Model/Sales/SalesInvoice/SalesInvoice.php index 2e64a5b5e..5bf413f94 100644 --- a/app/Model/Sales/SalesInvoice/SalesInvoice.php +++ b/app/Model/Sales/SalesInvoice/SalesInvoice.php @@ -102,6 +102,11 @@ public function payments() return $this->morphToMany(Payment::class, 'referenceable', 'payment_details')->active(); } + public function references() + { + return $this->hasMany(SalesInvoiceReference::class); + } + public function detachDownPayments() { $this->downPayments()->detach(); @@ -293,6 +298,18 @@ private static function setDownPaymentsDone($downPayments) } } + /** + * Get sales invoice reference. + */ + public static function getAvailable($salesInvoice) + { + $available = $salesInvoice->amount; + foreach ($salesInvoice->references as $reference) { + $available = $available - $reference->amount; + } + return $available; + } + private static function updateJournal($salesInvoice) { /** diff --git a/app/Model/Sales/SalesInvoice/SalesInvoiceReference.php b/app/Model/Sales/SalesInvoice/SalesInvoiceReference.php new file mode 100644 index 000000000..04e1ae2b0 --- /dev/null +++ b/app/Model/Sales/SalesInvoice/SalesInvoiceReference.php @@ -0,0 +1,49 @@ + 'double', + ]; + + public static function referenceableIsValid($value) + { + $referenceableTypes = [ + SalesReturn::$morphName, + ]; + + return in_array($value, $referenceableTypes); + } + + /** + * Get all of the owning referenceable models. + */ + public function referenceable() + { + return $this->morphTo(); + } + + public function salesInvoice() + { + return $this->belongsTo(SalesInvoice::class); + } +} diff --git a/app/Model/Sales/SalesReturn/SalesReturn.php b/app/Model/Sales/SalesReturn/SalesReturn.php index afbed9474..887524e95 100644 --- a/app/Model/Sales/SalesReturn/SalesReturn.php +++ b/app/Model/Sales/SalesReturn/SalesReturn.php @@ -3,14 +3,19 @@ namespace App\Model\Sales\SalesReturn; use App\Model\Form; +use App\Model\Token; use App\Model\TransactionModel; use App\Model\Accounting\Journal; use App\Model\Master\Item; +use App\Model\Sales\SalesInvoice\SalesInvoice; use App\Traits\Model\Sales\SalesReturnRelation; use App\Traits\Model\Sales\SalesReturnJoin; use Exception; use App\Helpers\Inventory\InventoryHelper; use App\Exceptions\IsReferencedException; +use Illuminate\Support\Facades\Mail; +use App\Mail\Sales\SalesReturnApprovalRequest; +use App\Model\Sales\SalesInvoice\SalesInvoiceReference; class SalesReturn extends TransactionModel { @@ -55,6 +60,7 @@ class SalesReturn extends TransactionModel public static function create($data) { + self::validate($data); $salesReturn = new self; $salesReturn->fill($data); @@ -65,14 +71,64 @@ public static function create($data) $salesReturn->save(); $salesReturn->items()->saveMany($items); + + self::checkJournalBalance($salesReturn); //$salesReturn->services()->saveMany($services); $form = new Form; $form->saveData($data, $salesReturn); + self::sendApproval($salesReturn); + return $salesReturn; } + private static function validate($data) + { + $salesInvoice = SalesInvoice::where('id', $data['sales_invoice_id'])->first(); + if ($salesInvoice->form->done === 1) { + throw new Exception('Sales return form already done', 422); + } + + $subTotal = 0; + foreach ($data['items'] as $item) { + $total = round($item['quantity'] * ($item['price'] - $item['discount_value']), 12); + if ($total != round($item['total'], 10)) { + throw new Exception('total for item ' .$item['item_name']. ' should be ' .$total , 422); + } + $subTotal = $subTotal + $total; + } + + if (round($data['sub_total'], 10) != round($subTotal, 10)) { + throw new Exception('sub total should be ' .$subTotal , 422); + } + + $taxBase = $subTotal; + if (round($data['tax_base'], 10) != round($taxBase, 10)) { + throw new Exception('tax base should be ' .$taxBase , 422); + } + + if ($data['type_of_tax'] != $salesInvoice->type_of_tax) { + throw new Exception('type of tax should be same with invoice' , 422); + } + + $tax = round($taxBase * (10 / 110), 10); + if (round($data['tax'], 10) != $tax) { + throw new Exception('tax should be ' .$tax , 422); + } + + $total = 0; + if ($data['type_of_tax'] === 'include') { + $total = $taxBase; + } else { + $total = $taxBase + $tax ; + } + + if (round($data['amount'], 10) != round($total, 10)) { + throw new Exception('amount should be ' .$total , 422); + } + } + private static function mapItems($items) { return array_map(function ($item) { @@ -154,6 +210,29 @@ public static function updateInventory($form, $salesReturn) } } + public static function checkJournalBalance($salesReturn) { + $ar = get_setting_journal('sales', 'account receivable'); + $salesIncome = get_setting_journal('sales', 'sales income'); + + $credit = $salesReturn->amount; + $debit = $salesReturn->amount - $salesReturn->tax; + foreach ($salesReturn->items as $item) { + $amount = $item->item->cogs($item->item_id) * $item->quantity; + $debit = $debit + $amount; + $credit = $credit + $amount; + } + + $debit = $debit + $salesReturn->tax; + if (round($debit, 10) != round($credit, 10)) { + throw new Exception('Journal not balance', 422); + } + + return [ + 'debit' => $debit, + 'credit' => $credit + ]; + } + public static function updateJournal($salesReturn) { $accountReceivable = new Journal; @@ -199,21 +278,94 @@ public static function updateJournal($salesReturn) } } + public static function updateSalesInvoiceReference($salesReturn) + { + $invoiceReference = new SalesInvoiceReference; + $invoiceReference->sales_invoice_id = $salesReturn->sales_invoice_id; + $invoiceReference->referenceable_id = $salesReturn->id; + $invoiceReference->referenceable_type = 'SalesReturn'; + $invoiceReference->amount = $salesReturn->amount; + $invoiceReference->save(); + } + public function isAllowedToUpdate() { $this->isNotReferenced(); + $this->isNotDone(); } public function isAllowedToDelete() { $this->isNotReferenced(); + $this->isNotDone(); } private function isNotReferenced() { // Check if not referenced by payments if ($this->paymentCollections->count()) { - throw new IsReferencedException('Cannot edit form because referenced by payment collection', $this->paymentCollections); + throw new IsReferencedException('form referenced by payment collection', $this->paymentCollections); + } + } + + private function isNotDone() + { + if ($this->form->done === 1) { + throw new Exception('form already done', 422); + } + } + + public static function sendApproval($salesReturns) + { + $salesReturnByApprovers = []; + + $sendBy = tenant(auth()->user()->id); + + $salesReturnByApprovers[$salesReturns->form->request_approval_to][] = $salesReturns; + + foreach ($salesReturnByApprovers as $salesReturnByApprover) { + $approver = null; + + $formStart = head($salesReturnByApprover)->form; + $formEnd = last($salesReturnByApprover)->form; + + $form = [ + 'number' => $formStart->number, + 'date' => $formStart->date, + 'created' => $formStart->created_at, + 'send_by' => $sendBy + ]; + + // loop each sales return by group approver + foreach ($salesReturnByApprover as $salesReturn) { + $salesReturn->action = 'create'; + + if(!$approver) { + $approver = $salesReturn->form->requestApprovalTo; + // create token based on request_approval_to + $approverToken = Token::where('user_id', $approver->id)->first(); + if (!$approverToken) { + $approverToken = new Token(); + $approverToken->user_id = $approver->id; + $approverToken->token = md5($approver->email.''.now()); + $approverToken->save(); + } + + $approver->token = $approverToken->token; + } + + if ($salesReturn->form->close_status === 0) $salesReturn->action = 'close'; + + if ( + $salesReturn->form->cancellation_status === 0 + && $salesReturn->form->close_status === null + ) { + $salesReturn->action = 'delete'; + } + } + + $approvalRequest = new SalesReturnApprovalRequest($salesReturnByApprover, $approver, (object) $form, $_SERVER['HTTP_REFERER']); + Mail::to([ $approver->email ])->queue($approvalRequest); } } } diff --git a/database/migrations/tenant/2022_12_15_064705_create_sales_invoice_references.php b/database/migrations/tenant/2022_12_15_064705_create_sales_invoice_references.php new file mode 100644 index 000000000..73c890482 --- /dev/null +++ b/database/migrations/tenant/2022_12_15_064705_create_sales_invoice_references.php @@ -0,0 +1,35 @@ +increments('id'); + $table->unsignedInteger('sales_invoice_id')->index(); + $table->foreign('sales_invoice_id')->references('id')->on('sales_invoices')->onDelete('restrict'); + $table->string('referenceable_type')->nullable(); + $table->string('referenceable_id')->nullable(); + $table->decimal('amount', 65, 30); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('sales_invoice_references'); + } +} diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php index 5d1f55ad7..8033609cb 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php @@ -7,12 +7,14 @@ use App\Model\Sales\SalesReturn\SalesReturn; use App\Model\Token; use App\User; +use App\Helpers\Inventory\InventoryHelper; class SalesReturnApprovalByEmailTest extends TestCase { use SalesReturnSetup; public static $path = '/api/v1/sales/return'; + public static $paycolPath = '/api/v1/sales/payment-collection'; private function findOrCreateToken($tenantUser) { @@ -46,108 +48,42 @@ private function changeActingAs($tenantUser, $salesReturn) $this->actingAs($user, 'api'); } - /** @test */ - public function success_create_sales_return() + public function create_sales_return() { $this->setRole(); $data = $this->getDummyData(); - $response = $this->json('POST', self::$path, $data, $this->headers); - - $response->assertStatus(201) - ->assertJson([ - "data" => [ - "id" => $response->json('data.id'), - "form" => [ - "id" => $response->json('data.form.id'), - "date" => $response->json('data.form.date'), - "number" => $response->json('data.form.number'), - "notes" => $response->json('data.form.notes'), - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_status' => 0, - 'done' => 0, - ], 'tenant'); + $this->json('POST', self::$path, $data, $this->headers); } - /** @test */ - public function success_delete_sales_return() + public function delete_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data['reason'] = $this->faker->text(200); - $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); - - $response->assertStatus(204); - $this->assertDatabaseHas('forms', [ - 'number' => $salesReturn->form->number, - 'request_cancellation_reason' => $data['reason'], - 'cancellation_status' => 0, - ], 'tenant'); + $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); } - /** @test */ - public function success_approve_sales_return() + public function approve_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); - $response->assertStatus(200) - ->assertJson([ - "data" => [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_status' => 1 - ], 'tenant'); - $this->assertDatabaseHas('user_activities', [ - 'number' => $response->json('data.form.number'), - 'table_id' => $response->json('data.id'), - 'table_type' => 'SalesReturn', - 'activity' => 'Approved' - ], 'tenant'); + $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); } /** @test */ - public function unauthorized_approve_by_email_sales_return() + public function error_already_approved_approve_by_email_sales_return() { - $this->success_delete_sales_return(); - - $this->unsetUserRole(); - - $response = $this->json('POST', self::$path . '/approve', [], $this->headers); - - $response->assertStatus(500) - ->assertJson([ - "code" => 0, - "message" => "There is no permission named `approve sales return` for guard `api`." - ]); - } - - /** @test */ - public function success_approve_by_email_sales_return() - { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturn->form->approval_status = 1; + $salesReturn->form->save(); $approver = $salesReturn->form->requestApprovalTo; $approverToken = $this->findOrCreateToken($approver); @@ -165,55 +101,46 @@ public function success_approve_by_email_sales_return() 'crud-type' => 'delete' ]; - $response = $this->json('POST', self::$path . '/approve', $data, $this->headers); - $response->assertStatus(200) - ->assertJson([ - "data" => [ - [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - "approval_status" => 1, - ] - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'id' => $salesReturn->form->id, - 'number' => $salesReturn->form->number, - 'approval_status' => 1 - ], 'tenant'); - $this->assertDatabaseHas('user_activities', [ - 'number' => $salesReturn->form->number, - 'table_id' => $salesReturn->id, - 'table_type' => 'SalesReturn', - 'activity' => 'Approved by Email' - ], 'tenant'); - $this->assertDatabaseHas('inventories', [ - 'form_id' => $salesReturn->form->id, - 'item_id' => $salesReturn->items()->first()->item_id, - 'quantity' => $salesReturn->items()->first()->quantity, - ], 'tenant'); + $response = $this->json('POST', self::$path . '/approve', $data , $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form '.$salesReturn->form->number.' already approved' + ]); } /** @test */ - public function success_approve_delete_by_email_sales_return() + public function unauthorized_approve_by_email_sales_return() + { + $this->create_sales_return(); + + $this->unsetUserRole(); + + $response = $this->json('POST', self::$path . '/approve', [], $this->headers); + + $response->assertStatus(500) + ->assertJson([ + "code" => 0, + "message" => "There is no permission named `approve sales return` for guard `api`." + ]); + } + + /** @test */ + public function success_approve_by_email_sales_return() { - $this->success_delete_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - $approver = $salesReturn->form->requestCancellationTo; + $approver = $salesReturn->form->requestApprovalTo; $approverToken = $this->findOrCreateToken($approver); $this->changeActingAs($approver, $salesReturn); $data = [ 'action' => 'approve', - 'approver_id' => $salesReturn->form->request_cancellation_to, + 'approver_id' => $salesReturn->form->request_approval_to, 'token' => $approverToken->token, 'resource-type' => 'SalesReturn', 'ids' => [ @@ -222,39 +149,119 @@ public function success_approve_delete_by_email_sales_return() 'crud-type' => 'delete' ]; - $response = $this->json('POST', self::$path . '/approve', $data, $this->headers); + $salesReturnItem = $salesReturn->items[0]; + $stock = InventoryHelper::getCurrentStock($salesReturnItem->item, $salesReturn->form->date, $salesReturn->warehouse, [ + 'expiry_date' => $salesReturnItem->item->expiry_date, + 'production_number' => $salesReturnItem->item->production_number, + ]); + + $response = $this->json('POST', self::$path . '/approve', $data, $this->headers); + $salesReturn = SalesReturn::where('id', $salesReturn->id)->first(); $response->assertStatus(200) ->assertJson([ - "data" => [ + 'data' => [ [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - "cancellation_status" => 1, + 'id' => $salesReturn->id, + 'sales_invoice_id' => $salesReturn->sales_invoice_id, + 'warehouse_id' => $salesReturn->warehouse_id, + 'customer_id' => $salesReturn->customer_id, + 'customer_name' => $salesReturn->customer_name, + 'customer_address' => $salesReturn->customer_address, + 'customer_phone' => $salesReturn->customer_phone, + 'tax' => $salesReturn->tax, + 'amount' => $salesReturn->amount, + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.0.form.date'), + 'number' => $salesReturn->form->number, + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $salesReturn->form->notes, + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.0.form.updated_by'), + 'done' => $salesReturn->form->done, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $salesReturn->form->formable_id, + 'formable_type' => $salesReturn->form->formable_type, + 'request_approval_at' => $response->json('data.0.form.request_approval_at'), + 'request_approval_to' => $salesReturn->form->request_approval_to, + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.0.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, + 'approval_status' => 1, + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.0.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.0.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => $salesReturn->form->cancellation_status, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.0.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.0.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, ] - ] + ] ] ]); + + $subTotal = $response->json('data.0.amount') - $response->json('data.0.tax'); $this->assertDatabaseHas('forms', [ - 'number' => $salesReturn->form->number, - 'cancellation_status' => 1, + 'id' => $response->json('data.0.form.id'), + 'number' => $response->json('data.0.form.number'), + 'approval_status' => 1 ], 'tenant'); + $this->assertDatabaseHas('user_activities', [ - 'number' => $salesReturn->form->number, - 'table_id' => $salesReturn->id, + 'number' => $response->json('data.0.form.number'), + 'table_id' => $response->json('data.0.id'), 'table_type' => 'SalesReturn', - 'activity' => 'Cancellation Approved by Email' + 'activity' => 'Approved By Email' + ], 'tenant'); + + $this->assertDatabaseHas('journals', [ + 'form_id' => $response->json('data.0.form.id'), + 'chart_of_account_id' => $this->arCoa->id, + 'credit' => $response->json('data.0.amount').'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseHas('journals', [ + 'form_id' => $response->json('data.0.form.id'), + 'chart_of_account_id' => $this->salesIncomeCoa->id, + 'debit' => $subTotal.'.000000000000000000000000000000' ], 'tenant'); + $this->assertDatabaseHas('journals', [ + 'form_id' => $response->json('data.0.form.id'), + 'chart_of_account_id' => $this->taxCoa->id, + 'debit' => $response->json('data.0.tax').'.000000000000000000000000000000' + ], 'tenant'); + + $stockNew = InventoryHelper::getCurrentStock($salesReturnItem->item, $salesReturn->form->date, $salesReturn->warehouse, [ + 'expiry_date' => $salesReturnItem->item->expiry_date, + 'production_number' => $salesReturnItem->item->production_number, + ]); + $this->assertEquals($stockNew, ($stock + $salesReturnItem->quantity)); + + $referenced = $this->json('GET', self::$paycolPath. '/'.$salesReturn->customer_id.'/references', [], $this->headers); + $referenced->assertStatus(200) + ->assertJson([ + 'data' => [ + 'salesReturn' => [ + [ 'number' => $salesReturn->form->number ] + ] + ] + ]); } - - /** @test */ + + /** @test */ public function unauthorized_reject_by_email_sales_return() { - $this->success_delete_sales_return(); + $this->create_sales_return(); $this->unsetUserRole(); @@ -268,9 +275,9 @@ public function unauthorized_reject_by_email_sales_return() } /** @test */ - public function success_reject_by_email_sales_return() + public function success_reject_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -287,90 +294,94 @@ public function success_reject_by_email_sales_return() 'ids' => [ ['id' => $salesReturn->id] ], - 'crud-type' => 'delete' + 'crud-type' => 'delete', + 'reason' => $this->faker->text(200) ]; $response = $this->json('POST', self::$path . '/reject', $data, $this->headers); - + $salesReturn = SalesReturn::where('id', $salesReturn->id)->first(); $response->assertStatus(200) - ->assertJson([ - "data" => [ - [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - "approval_status" => -1, - ] - ] + ->assertJson([ + 'data' => [ + [ + 'id' => $salesReturn->id, + 'sales_invoice_id' => $salesReturn->sales_invoice_id, + 'warehouse_id' => $salesReturn->warehouse_id, + 'customer_id' => $salesReturn->customer_id, + 'customer_name' => $salesReturn->customer_name, + 'customer_address' => $salesReturn->customer_address, + 'customer_phone' => $salesReturn->customer_phone, + 'tax' => $salesReturn->tax, + 'amount' => $salesReturn->amount, + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.0.form.date'), + 'number' => $salesReturn->form->number, + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $salesReturn->form->notes, + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.0.form.updated_by'), + 'done' => $salesReturn->form->done, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $salesReturn->form->formable_id, + 'formable_type' => $salesReturn->form->formable_type, + 'request_approval_at' => $response->json('data.0.form.request_approval_at'), + 'request_approval_to' => $salesReturn->form->request_approval_to, + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.0.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, + 'approval_status' => -1, + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.0.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.0.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => $salesReturn->form->cancellation_status, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.0.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.0.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, + ] ] - ]); + ] + ]); + $this->assertDatabaseHas('forms', [ - 'id' => $salesReturn->form->id, - 'number' => $salesReturn->form->number, + 'id' => $response->json('data.0.form.id'), + 'number' => $response->json('data.0.form.number'), 'approval_status' => -1, 'done' => 0, ], 'tenant'); + $this->assertDatabaseHas('user_activities', [ - 'number' => $salesReturn->form->number, - 'table_id' => $salesReturn->id, + 'number' => $response->json('data.0.form.number'), + 'table_id' => $response->json('data.0.id'), 'table_type' => 'SalesReturn', - 'activity' => 'Rejected by Email' + 'activity' => 'Rejected By Email' ], 'tenant'); - } - /** @test */ - public function success_reject_delete_by_email_sales_return() - { - $this->success_delete_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $approver = $salesReturn->form->requestCancellationTo; - $approverToken = $this->findOrCreateToken($approver); - - $this->changeActingAs($approver, $salesReturn); - - $data = [ - 'action' => 'reject', - 'approver_id' => $salesReturn->form->request_cancellation_to, - 'token' => $approverToken->token, - 'resource-type' => 'SalesReturn', - 'ids' => [ - ['id' => $salesReturn->id] - ], - 'crud-type' => 'delete' - ]; - - $response = $this->json('POST', self::$path . '/reject', $data, $this->headers); - - $response->assertStatus(200) - ->assertJson([ - "data" => [ - [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - "cancellation_status" => -1, - ] - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'number' => $salesReturn->form->number, - 'cancellation_status' => -1, - 'done' => 0 + $subTotal = $response->json('data.0.amount') - $response->json('data.0.tax'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.0.form.id'), + 'chart_of_account_id' => $this->arCoa->id, + 'credit' => $response->json('data.0.amount').'.000000000000000000000000000000' ], 'tenant'); - $this->assertDatabaseHas('user_activities', [ - 'number' => $salesReturn->form->number, - 'table_id' => $salesReturn->id, - 'table_type' => 'SalesReturn', - 'activity' => 'Cancellation Rejected by Email' + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.0.form.id'), + 'chart_of_account_id' => $this->salesIncomeCoa->id, + 'debit' => $subTotal.'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.0.form.id'), + 'chart_of_account_id' => $this->taxCoa->id, + 'debit' => $response->json('data.0.tax').'.000000000000000000000000000000' ], 'tenant'); } } diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php index c5630c4e4..875624797 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php @@ -6,278 +6,428 @@ use App\Model\Form; use App\Model\Sales\SalesReturn\SalesReturn; +use App\Helpers\Inventory\InventoryHelper; class SalesReturnApprovalTest extends TestCase { - use SalesReturnSetup; - - public static $path = '/api/v1/sales/return'; - - private $previousSalesReturnData; - - /** @test */ - public function success_create_sales_return($isFirstCreate = true) - { - $data = $this->getDummyData(); - - if($isFirstCreate) { - $this->setRole(); - $this->previousSalesReturnData = $data; - } - - $response = $this->json('POST', self::$path, $data, $this->headers); - - $response->assertStatus(201) - ->assertJson([ - "data" => [ - "id" => $response->json('data.id'), - "form" => [ - "id" => $response->json('data.form.id'), - "date" => $response->json('data.form.date'), - "number" => $response->json('data.form.number'), - "notes" => $response->json('data.form.notes'), - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_status' => 0, - 'done' => 0, - ], 'tenant'); - } - - /** @test */ - public function unauthorized_approve_sales_return() - { - $this->success_create_sales_return(); - - $this->unsetUserRole(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); - - $response->assertStatus(500) - ->assertJson([ - "code" => 0, - "message" => "There is no permission named `approve sales return` for guard `api`." - ]); - } - - /** @test */ - public function success_approve_sales_return() - { - $this->success_create_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); - $response->assertStatus(200) - ->assertJson([ - "data" => [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - "approval_status" => 1, - ] - ] - ]); - $subTotal = $response->json('data.amount') - $response->json('data.tax'); - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_status' => 1 - ], 'tenant'); - $this->assertDatabaseHas('user_activities', [ - 'number' => $response->json('data.form.number'), - 'table_id' => $response->json('data.id'), - 'table_type' => 'SalesReturn', - 'activity' => 'Approved' - ], 'tenant'); - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_status' => 1 - ], 'tenant'); - $this->assertDatabaseHas('journals', [ - 'form_id' => $response->json('data.form.id'), - 'chart_of_account_id' => $this->arCoa->id, - 'credit' => $response->json('data.amount').'.000000000000000000000000000000' - ], 'tenant'); - $this->assertDatabaseHas('journals', [ - 'form_id' => $response->json('data.form.id'), - 'chart_of_account_id' => $this->salesIncomeCoa->id, - 'debit' => $subTotal.'.000000000000000000000000000000' - ], 'tenant'); - $this->assertDatabaseHas('journals', [ - 'form_id' => $response->json('data.form.id'), - 'chart_of_account_id' => $this->taxCoa->id, - 'debit' => $response->json('data.tax').'.000000000000000000000000000000' - ], 'tenant'); - } - - /** @test */ - public function unauthorized_reject_sales_return() - { - $this->success_create_sales_return(); - - $this->unsetUserRole(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/reject', [], $this->headers); - - $response->assertStatus(500) - ->assertJson([ - "code" => 0, - "message" => "There is no permission named `approve sales return` for guard `api`." - ]); - } - - /** @test */ - public function invalid_reject_sales_return() - { - $this->success_create_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/reject', [], $this->headers); - - $response->assertStatus(422) - ->assertJson([ - "code" => 422, - "message" => "The given data was invalid." - ]); - } - - /** @test */ - public function success_reject_sales_return() - { - $this->success_create_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - $data['reason'] = $this->faker->text(200); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/reject', $data, $this->headers); - - $response->assertStatus(200) - ->assertJson([ - "data" => [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - "approval_status" => -1, - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), + use SalesReturnSetup; + + public static $path = '/api/v1/sales/return'; + public static $paycolPath = '/api/v1/sales/payment-collection'; + + private $previousSalesReturnData; + + public function create_sales_return($isFirstCreate = true) + { + $data = $this->getDummyData(); + + if($isFirstCreate) { + $this->setRole(); + $this->previousSalesReturnData = $data; + } + + $this->json('POST', self::$path, $data, $this->headers); + } + + /** @test */ + public function error_already_approved_approve_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturn->form->approval_status = 1; + $salesReturn->form->save(); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form already approved' + ]); + } + + /** @test */ + public function unauthorized_approve_sales_return() + { + $this->create_sales_return(); + + $this->unsetUserRole(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); + + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `approve sales return` for guard `api`.' + ]); + } + + /** @test */ + public function success_approve_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturnItem = $salesReturn->items[0]; + + $stock = InventoryHelper::getCurrentStock($salesReturnItem->item, $salesReturn->form->date, $salesReturn->warehouse, [ + 'expiry_date' => $salesReturnItem->item->expiry_date, + 'production_number' => $salesReturnItem->item->production_number, + ]); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); + + $salesReturn->refresh(); + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + 'id' => $salesReturn->id, + 'sales_invoice_id' => $salesReturn->sales_invoice_id, + 'warehouse_id' => $salesReturn->warehouse_id, + 'customer_id' => $salesReturn->customer_id, + 'customer_name' => $salesReturn->customer_name, + 'customer_address' => $salesReturn->customer_address, + 'customer_phone' => $salesReturn->customer_phone, + 'tax' => $salesReturn->tax, + 'amount' => $salesReturn->amount, + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.form.date'), + 'number' => $salesReturn->form->number, + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $salesReturn->form->notes, + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.form.updated_by'), + 'done' => $salesReturn->form->done, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $salesReturn->form->formable_id, + 'formable_type' => $salesReturn->form->formable_type, + 'request_approval_at' => $response->json('data.form.request_approval_at'), + 'request_approval_to' => $salesReturn->form->request_approval_to, + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, + 'approval_status' => 1, + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => $salesReturn->form->cancellation_status, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, + ] + ] + ]); + + $subTotal = $response->json('data.amount') - $response->json('data.tax'); + $this->assertDatabaseHas('forms', [ + 'id' => $response->json('data.form.id'), + 'number' => $response->json('data.form.number'), + 'approval_status' => 1 + ], 'tenant'); + + $this->assertDatabaseHas('user_activities', [ + 'number' => $response->json('data.form.number'), + 'table_id' => $response->json('data.id'), + 'table_type' => 'SalesReturn', + 'activity' => 'Approved' + ], 'tenant'); + + $this->assertDatabaseHas('journals', [ + 'form_id' => $response->json('data.form.id'), + 'chart_of_account_id' => $this->arCoa->id, + 'credit' => $response->json('data.amount').'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseHas('journals', [ + 'form_id' => $response->json('data.form.id'), + 'chart_of_account_id' => $this->salesIncomeCoa->id, + 'debit' => $subTotal.'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseHas('journals', [ + 'form_id' => $response->json('data.form.id'), + 'chart_of_account_id' => $this->taxCoa->id, + 'debit' => $response->json('data.tax').'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseHas('sales_invoice_references', [ + 'referenceable_id' => $response->json('data.id'), + 'referenceable_type' => 'SalesReturn', + 'amount' => $response->json('data.amount').'.00000000000000000000000000000' + ], 'tenant'); + + $stockNew = InventoryHelper::getCurrentStock($salesReturnItem->item, $salesReturn->form->date, $salesReturn->warehouse, [ + 'expiry_date' => $salesReturnItem->item->expiry_date, + 'production_number' => $salesReturnItem->item->production_number, + ]); + $this->assertEquals($stockNew, ($stock + $salesReturnItem->quantity)); + + $referenced = $this->json('GET', self::$paycolPath. '/'.$salesReturn->customer_id.'/references', [], $this->headers); + $referenced->assertStatus(200) + ->assertJson([ + 'data' => [ + 'salesReturn' => [ + [ 'number' => $salesReturn->form->number ] + ] + ] + ]); + } + + /** @test */ + public function error_reason_more_than_255_character_reject_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(500); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/reject', $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + 'errors' => [ + 'reason' => [ + 'The reason may not be greater than 255 characters.' + ] + ] + ]); + } + + /** @test */ + public function error_empty_reason_reject_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/reject', [], $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + 'errors' => [ + 'reason' => [ + 'The reason field is required.' + ] + ] + ]); + } + + /** @test */ + public function unauthorized_reject_sales_return() + { + $this->create_sales_return(); + + $this->unsetUserRole(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/reject', [], $this->headers); + + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `approve sales return` for guard `api`.' + ]); + } + + /** @test */ + public function success_reject_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(200); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/reject', $data, $this->headers); + + $salesReturn->refresh(); + + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + 'id' => $salesReturn->id, + 'sales_invoice_id' => $salesReturn->sales_invoice_id, + 'warehouse_id' => $salesReturn->warehouse_id, + 'customer_id' => $salesReturn->customer_id, + 'customer_name' => $salesReturn->customer_name, + 'customer_address' => $salesReturn->customer_address, + 'customer_phone' => $salesReturn->customer_phone, + 'tax' => $salesReturn->tax, + 'amount' => $salesReturn->amount, + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.form.date'), + 'number' => $salesReturn->form->number, + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $salesReturn->form->notes, + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.form.updated_by'), + 'done' => $salesReturn->form->done, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $salesReturn->form->formable_id, + 'formable_type' => $salesReturn->form->formable_type, + 'request_approval_at' => $response->json('data.form.request_approval_at'), + 'request_approval_to' => $salesReturn->form->request_approval_to, + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, 'approval_status' => -1, - 'done' => 0, - ], 'tenant'); - $this->assertDatabaseHas('user_activities', [ - 'number' => $response->json('data.form.number'), - 'table_id' => $response->json('data.id'), - 'table_type' => 'SalesReturn', - 'activity' => 'Rejected' - ], 'tenant'); - } - - /** @test */ - public function success_read_approval_sales_return() - { - $this->success_create_sales_return(); - - $data = [ - 'join' => 'form,customer,items,item', - 'fields' => 'sales_return.*', - 'sort_by' => '-form.number', - 'group_by' => 'form.id', - 'filter_form'=>'notArchived;null', - 'filter_like'=>'{}', - 'filter_date_min'=>'{"form.date":"2022-05-01 00:00:00"}', - 'filter_date_max'=>'{"form.date":"2022-05-17 23:59:59"}', - 'includes'=>'form;customer;warehouse;items.item;items.allocation', - 'limit'=>10, - 'page' => 1 - ]; - - $response = $this->json('GET', self::$path . '/approval', $data, $this->headers); - - $response->assertStatus(200) - ->assertJsonStructure([ - "data" => [ - [ - "id", - "last_request_date", - "items" => [ - [ - "item_name", - "quantity", - ] - ], - "form" => [ - "number", - "date", - ] - ] - ] - ]); - $this->assertGreaterThan(0, count($response->json('data'))); - } - - /** @test */ - public function success_send_approval_sales_return() - { - $this->success_create_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - $data['ids'][] = ['id' => $salesReturn->id]; - - $response = $this->json('POST', self::$path . '/approval/send', $data, $this->headers); - - $response->assertStatus(200) - ->assertJson([ - "input" => [ - "ids" => [ - [ "id" => $salesReturn->id ] - ] - ] - ]); - - } - - /** @test */ - public function success_send_multiple_approval_sales_return() - { - $this->success_create_sales_return(); - - $this->success_create_sales_return($isFirstCreate = false); - $salesReturn = SalesReturn::orderBy('id', 'desc')->first(); - $salesReturn->form->cancellation_status = 0; - $salesReturn->form->close_status = null; - $salesReturn->form->save(); - - $data['ids'] = SalesReturn::get() - ->pluck('id') - ->map(function ($id) { return ['id' => $id]; }) - ->toArray(); - - $response = $this->json('POST', self::$path . '/approval/send', $data, $this->headers); - - $response->assertStatus(200) - ->assertJson([ - "input" => [ - "ids" => $data['ids'] + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => $salesReturn->form->cancellation_status, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, + ] + ] + ]); + + $this->assertDatabaseHas('forms', [ + 'id' => $response->json('data.form.id'), + 'number' => $response->json('data.form.number'), + 'approval_status' => -1, + 'done' => 0, + ], 'tenant'); + + $this->assertDatabaseHas('user_activities', [ + 'number' => $response->json('data.form.number'), + 'table_id' => $response->json('data.id'), + 'table_type' => 'SalesReturn', + 'activity' => 'Rejected' + ], 'tenant'); + + $subTotal = $response->json('data.amount') - $response->json('data.tax'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.form.id'), + 'chart_of_account_id' => $this->arCoa->id, + 'credit' => $response->json('data.amount').'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.form.id'), + 'chart_of_account_id' => $this->salesIncomeCoa->id, + 'debit' => $subTotal.'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.form.id'), + 'chart_of_account_id' => $this->taxCoa->id, + 'debit' => $response->json('data.tax').'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseMissing('user_activities', [ + 'number' => $response->json('data.form.number'), + 'table_id' => $response->json('data.id'), + 'table_type' => 'SalesReturn', + 'activity' => 'Cancel Approved' + ], 'tenant'); + } + + /** @test */ + public function error_no_branch_send_approval_sales_return() + { + $this->create_sales_return(); + + $this->branchDefault->pivot->is_default = false; + $this->branchDefault->pivot->save(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['ids'][] = ['id' => $salesReturn->id]; + + $response = $this->json('POST', self::$path . '/approval/send', $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'please set default branch to create this form' + ]); + } + + /** @test */ + public function unauthorized_send_approval_sales_return() + { + $this->create_sales_return(); + + $this->unsetUserRole(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['ids'][] = ['id' => $salesReturn->id]; + + $response = $this->json('POST', self::$path . '/approval/send', $data, $this->headers); + + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `create sales return` for guard `api`.' + ]); + } + + /** @test */ + public function success_send_approval_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['ids'][] = ['id' => $salesReturn->id]; + + $response = $this->json('POST', self::$path . '/approval/send', $data, $this->headers); + + $response->assertStatus(200) + ->assertJson([ + "input" => [ + "ids" => [ + [ "id" => $salesReturn->id ] ] - ]); - } + ] + ]); + } + + /** @test */ + public function success_send_multiple_approval_sales_return() + { + $this->create_sales_return(); + + $this->create_sales_return($isFirstCreate = false); + $salesReturn = SalesReturn::orderBy('id', 'desc')->first(); + $salesReturn->form->cancellation_status = 0; + $salesReturn->form->close_status = null; + $salesReturn->form->save(); + + $data['ids'] = SalesReturn::get() + ->pluck('id') + ->map(function ($id) { return ['id' => $id]; }) + ->toArray(); + + $response = $this->json('POST', self::$path . '/approval/send', $data, $this->headers); + + $response->assertStatus(200) + ->assertJson([ + "input" => [ + "ids" => $data['ids'] + ] + ]); + } } \ No newline at end of file diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php index 4c603cd5f..3098b9348 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php @@ -5,213 +5,335 @@ use Tests\TestCase; use App\Model\Sales\SalesReturn\SalesReturn; +use App\Model\Sales\SalesInvoice\SalesInvoice; +use App\Helpers\Inventory\InventoryHelper; class SalesReturnCancellationApprovalTest extends TestCase { - use SalesReturnSetup; - - public static $path = '/api/v1/sales/return'; - - /** @test */ - public function success_create_sales_return() - { + use SalesReturnSetup; + + public static $path = '/api/v1/sales/return'; + + public function create_sales_return($isFirstCreate = true) + { + $data = $this->getDummyData(); + + if($isFirstCreate) { $this->setRole(); - - $data = $this->getDummyData(); - - $response = $this->json('POST', self::$path, $data, $this->headers); - - $response->assertStatus(201) - ->assertJson([ - "data" => [ - "id" => $response->json('data.id'), - "form" => [ - "id" => $response->json('data.form.id'), - "date" => $response->json('data.form.date'), - "number" => $response->json('data.form.number'), - "notes" => $response->json('data.form.notes'), - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_status' => 0, - 'done' => 0, - ], 'tenant'); - } - - /** @test */ - public function success_delete_sales_return() - { - $this->success_create_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - $data['reason'] = $this->faker->text(200); - - $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); - - $response->assertStatus(204); - $this->assertDatabaseHas('forms', [ - 'number' => $salesReturn->form->number, - 'request_cancellation_reason' => $data['reason'], - 'cancellation_status' => 0, - ], 'tenant'); - } - - /** @test */ - public function unauthorized_cancellation_approve_sales_return() - { - $this->success_delete_sales_return(); - - $this->unsetUserRole(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-approve', [], $this->headers); - - $response->assertStatus(500) - ->assertJson([ - "code" => 0, - "message" => "There is no permission named `approve sales return` for guard `api`." - ]); - } - - /** @test */ - public function invalid_state_cancellation_approve_sales_return() - { - $this->success_create_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-approve', [], $this->headers); - - $response->assertStatus(422) - ->assertJson([ - "code" => 422, - "message" => "form not in cancellation pending state" - ]); + $this->previousSalesReturnData = $data; } - /** @test */ - public function success_cancellation_approve_sales_return() - { - $this->success_delete_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-approve', [], $this->headers); - - $response->assertStatus(200) - ->assertJson([ - "data" => [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - "cancellation_status" => 1, - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'number' => $salesReturn->form->number, - 'cancellation_status' => 1, - ], 'tenant'); - $this->assertDatabaseHas('user_activities', [ - 'number' => $response->json('data.form.number'), - 'table_id' => $response->json('data.id'), - 'table_type' => 'SalesReturn', - 'activity' => 'Cancel Approved' - ], 'tenant'); - } - - /** @test */ - public function unauthorized_cancellation_reject_sales_return() - { - $this->success_delete_sales_return(); - - $this->unsetUserRole(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', [], $this->headers); - - $response->assertStatus(500) - ->assertJson([ - "code" => 0, - "message" => "There is no permission named `approve sales return` for guard `api`." - ]); - } - - /** @test */ - public function invalid_cancellation_reject_sales_return() - { - $this->success_delete_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', [], $this->headers); - - $response->assertStatus(422) - ->assertJson([ - "code" => 422, - "message" => "The given data was invalid." - ]); - } - - /** @test */ - public function invalid_state_cancellation_reject_sales_return() - { - $this->success_create_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $data['reason'] = $this->faker->text(200); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', $data, $this->headers); - - $response->assertStatus(422) - ->assertJson([ - "code" => 422, - "message" => "form not in cancellation pending state" - ]); - } - - /** @test */ - public function success_reject_sales_return() - { - $this->success_delete_sales_return(); - - $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - $data['reason'] = $this->faker->text(200); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', $data, $this->headers); - - $response->assertStatus(200) - ->assertJson([ - "data" => [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - "cancellation_status" => -1, - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'number' => $salesReturn->form->number, - 'cancellation_status' => -1, - 'done' => 0 - ], 'tenant'); - $this->assertDatabaseHas('user_activities', [ - 'number' => $response->json('data.form.number'), - 'table_id' => $response->json('data.id'), - 'table_type' => 'SalesReturn', - 'activity' => 'Cancel Rejected' - ], 'tenant'); - } + $this->json('POST', self::$path, $data, $this->headers); + } + + public function approve_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); + } + + public function delete_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(200); + + $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); + } + + public function delete_approved_sales_return() + { + $this->approve_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(200); + + $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); + } + + /** @test */ + public function error_already_cancelled_approve_sales_return() + { + $this->delete_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturn->form->cancellation_status = 1; + $salesReturn->form->save(); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-approve', [], $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form not in cancellation pending state' + ]); + } + + /** @test */ + public function unauthorized_approve_approve_cancel_sales_return() + { + $this->delete_sales_return(); + + $this->unsetUserRole(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-approve', [], $this->headers); + + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `approve sales return` for guard `api`.' + ]); + } + + /** @test */ + public function success_approve_cancel_sales_return() + { + $this->delete_approved_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturnItem = $salesReturn->items[0]; + + $stock = InventoryHelper::getCurrentStock($salesReturnItem->item, $salesReturn->form->date, $salesReturn->warehouse, [ + 'expiry_date' => $salesReturnItem->item->expiry_date, + 'production_number' => $salesReturnItem->item->production_number, + ]); + + $amountInvoice = SalesInvoice::getAvailable($salesReturn->salesInvoice); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-approve', [], $this->headers); + $salesReturn->refresh(); + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + 'id' => $salesReturn->id, + 'sales_invoice_id' => $salesReturn->sales_invoice_id, + 'warehouse_id' => $salesReturn->warehouse_id, + 'customer_id' => $salesReturn->customer_id, + 'customer_name' => $salesReturn->customer_name, + 'customer_address' => $salesReturn->customer_address, + 'customer_phone' => $salesReturn->customer_phone, + 'tax' => $salesReturn->tax, + 'amount' => $salesReturn->amount, + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.form.date'), + 'number' => $salesReturn->form->number, + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $salesReturn->form->notes, + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.form.updated_by'), + 'done' => $salesReturn->form->done, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $salesReturn->form->formable_id, + 'formable_type' => $salesReturn->form->formable_type, + 'request_approval_at' => $response->json('data.form.request_approval_at'), + 'request_approval_to' => $salesReturn->form->request_approval_to, + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, + 'approval_status' => $salesReturn->form->approval_status, + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => 1, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, + ] + ] + ]); + + $subTotal = $response->json('data.amount') - $response->json('data.tax'); + $this->assertDatabaseHas('forms', [ + 'id' => $response->json('data.form.id'), + 'number' => $response->json('data.form.number'), + 'cancellation_status' => 1 + ], 'tenant'); + + $this->assertDatabaseHas('user_activities', [ + 'number' => $response->json('data.form.number'), + 'table_id' => $response->json('data.id'), + 'table_type' => 'SalesReturn', + 'activity' => 'Cancel Approved' + ], 'tenant'); + + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.form.id'), + 'chart_of_account_id' => $this->arCoa->id, + 'credit' => $response->json('data.amount').'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.form.id'), + 'chart_of_account_id' => $this->salesIncomeCoa->id, + 'debit' => $subTotal.'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.form.id'), + 'chart_of_account_id' => $this->taxCoa->id, + 'debit' => $response->json('data.tax').'.000000000000000000000000000000' + ], 'tenant'); + + $stockNew = InventoryHelper::getCurrentStock($salesReturnItem->item, $salesReturn->form->date, $salesReturn->warehouse, [ + 'expiry_date' => $salesReturnItem->item->expiry_date, + 'production_number' => $salesReturnItem->item->production_number, + ]); + $this->assertEquals($stockNew, $stock - $salesReturnItem->quantity); + + $salesReturn->refresh(); + $amountInvoiceNew = SalesInvoice::getAvailable($salesReturn->salesInvoice); + $this->assertEquals($amountInvoiceNew, $amountInvoice + $salesReturn->amount); + } + + /** @test */ + public function success_reject_cancel_sales_return() + { + $this->delete_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturnItem = $salesReturn->items[0]; + + $data['reason'] = $this->faker->text(200); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', $data, $this->headers); + $salesReturn->refresh(); + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + 'id' => $salesReturn->id, + 'sales_invoice_id' => $salesReturn->sales_invoice_id, + 'warehouse_id' => $salesReturn->warehouse_id, + 'customer_id' => $salesReturn->customer_id, + 'customer_name' => $salesReturn->customer_name, + 'customer_address' => $salesReturn->customer_address, + 'customer_phone' => $salesReturn->customer_phone, + 'tax' => $salesReturn->tax, + 'amount' => $salesReturn->amount, + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.form.date'), + 'number' => $salesReturn->form->number, + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $salesReturn->form->notes, + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.form.updated_by'), + 'done' => $salesReturn->form->done, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $salesReturn->form->formable_id, + 'formable_type' => $salesReturn->form->formable_type, + 'request_approval_at' => $response->json('data.form.request_approval_at'), + 'request_approval_to' => $salesReturn->form->request_approval_to, + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, + 'approval_status' => $salesReturn->form->approval_status, + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => -1, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, + ] + ] + ]); + + $this->assertDatabaseHas('forms', [ + 'id' => $response->json('data.form.id'), + 'number' => $response->json('data.form.number'), + 'cancellation_status' => -1 + ], 'tenant'); + + $this->assertDatabaseHas('user_activities', [ + 'number' => $response->json('data.form.number'), + 'table_id' => $response->json('data.id'), + 'table_type' => 'SalesReturn', + 'activity' => 'Cancel Rejected' + ], 'tenant'); + } + + /** @test */ + public function error_reason_more_than_255_character_reject_cancel_sales_return() + { + $this->delete_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(500); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + 'errors' => [ + 'reason' => [ + 'The reason may not be greater than 255 characters.' + ] + ] + ]); + } + + /** @test */ + public function error_empty_reason_reject_cancel_sales_return() + { + $this->delete_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', [], $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + 'errors' => [ + 'reason' => [ + 'The reason field is required.' + ] + ] + ]); + } + + /** @test */ + public function unauthorized_reject_cancel_sales_return() + { + $this->delete_sales_return(); + + $this->unsetUserRole(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', [], $this->headers); + + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `approve sales return` for guard `api`.' + ]); + } } diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php index 64be38626..3a853f21a 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php @@ -13,70 +13,62 @@ class SalesReturnHistoryTest extends TestCase public static $path = '/api/v1/sales/return'; - /** @test */ - public function success_create_sales_return() + public function create_sales_return() { $this->setRole(); $data = $this->getDummyData(); - $response = $this->json('POST', self::$path, $data, $this->headers); - - $response->assertStatus(201) - ->assertJson([ - "data" => [ - "id" => $response->json('data.id'), - "form" => [ - "id" => $response->json('data.form.id'), - "date" => $response->json('data.form.date'), - "number" => $response->json('data.form.number'), - "notes" => $response->json('data.form.notes'), - ] - ] - ]); - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_status' => 0, - 'done' => 0, - ], 'tenant'); + $this->json('POST', self::$path, $data, $this->headers); } - /** @test */ - public function success_update_sales_return() + + public function update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data = $this->getDummyData($salesReturn); $data = data_set($data, 'id', $salesReturn->id, false); - $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + } - $response->assertStatus(201) - ->assertJson([ - "data" => [ - "id" => $response->json('data.id'), - "form" => [ - "id" => $response->json('data.form.id'), - "date" => $response->json('data.form.date'), - "number" => $response->json('data.form.number'), - "notes" => $response->json('data.form.notes'), - ] - ] - ]); - $this->assertDatabaseHas('forms', [ 'edited_number' => $response->json('data.form.number') ], 'tenant'); - $this->assertDatabaseHas('user_activities', [ - 'number' => $response->json('data.form.number'), - 'table_id' => $response->json('data.id'), - 'table_type' => 'SalesReturn', - 'activity' => 'Update - 1' - ], 'tenant'); + /** @test */ + public function unauthorized_no_default_branch_read_histories() + { + $this->update_sales_return(); + + $this->branchDefault->pivot->is_default = false; + $this->branchDefault->pivot->save(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturnUpdated = SalesReturn::orderBy('id', 'desc')->first(); + + $data = [ + 'sort_by' => '-user_activities.date', + 'includes' => 'user', + 'filter_like' => '{}', + 'or_filter_where_has_like[]' => '{"user":{}}', + 'limit' => 10, + 'page' => 1 + ]; + + $response = $this->json('GET', self::$path . '/' . $salesReturnUpdated->id . '/histories', $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'please set default branch to read this form' + ]); } + /** @test */ - public function read_sales_return_histories() + public function unauthorized_create_sales_return() { - $this->success_update_sales_return(); + $this->update_sales_return(); + + $this->unsetUserRole(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $salesReturnUpdated = SalesReturn::orderBy('id', 'desc')->first(); @@ -92,22 +84,60 @@ public function read_sales_return_histories() $response = $this->json('GET', self::$path . '/' . $salesReturnUpdated->id . '/histories', $data, $this->headers); + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `read sales return` for guard `api`.' + ]); + } + + /** @test */ + public function read_sales_return_histories() + { + $this->update_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturnUpdated = SalesReturn::orderBy('id', 'desc')->first(); + + $data = [ + 'sort_by' => '-user_activities.date', + 'includes' => 'user', + 'filter_like' => '{}', + 'or_filter_where_has_like[]' => '{"user":{}}', + 'limit' => 10, + 'page' => 1 + ]; + + $response = $this->json('GET', self::$path . '/' . $salesReturnUpdated->id . '/histories', $data, $this->headers); + $response->assertStatus(200) - ->assertJsonStructure([ - "data" => [ + ->assertJson([ + 'data' => [ [ - "id", - "table_type", - "table_id", - "number", - "date", - "user_id", - "activity", - "formable_id", - "user", + 'id' => $response->json('data.0.id'), + 'table_type' => 'SalesReturn', + 'table_id' => $salesReturnUpdated->id, + 'number' => $salesReturnUpdated->form->number, + 'date' => $response->json('data.0.date'), + 'user_id' => $response->json('data.0.user_id'), + 'activity' => $response->json('data.0.activity'), + 'formable_id' => $salesReturnUpdated->id, + 'user' => [ + 'id' => $response->json('data.0.user.id'), + 'name' => $response->json('data.0.user.name'), + 'first_name' => $response->json('data.0.user.first_name'), + 'last_name' => $response->json('data.0.user.last_name'), + 'address' => $response->json('data.0.user.address'), + 'phone' => $response->json('data.0.user.phone'), + 'email' => $response->json('data.0.user.email'), + 'branch_id' => $response->json('data.0.user.branch_id'), + 'warehouse_id' => $response->json('data.0.user.warehouse_id'), + 'full_name' => $response->json('data.0.user.full_name'), + ], ] ] ]); + $this->assertGreaterThan(0, count($response->json('data'))); $this->assertDatabaseHas('user_activities', [ 'number' => $salesReturn->form->edited_number, @@ -122,10 +152,11 @@ public function read_sales_return_histories() 'activity' => 'Update - 1' ], 'tenant'); } + /** @test */ public function success_create_sales_return_history() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data = [ diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnSetup.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnSetup.php index 0d929698d..1cf908b53 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnSetup.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnSetup.php @@ -21,6 +21,7 @@ use App\Model\Sales\PaymentCollection\PaymentCollection; use App\Model\SettingJournal; use App\Model\Accounting\Journal; +use App\Helpers\Inventory\InventoryHelper; trait SalesReturnSetup { private $tenantUser; @@ -65,6 +66,14 @@ private function setUserWarehouse($branch = null) $this->warehouseSelected = $warehouse; } } + + private function removeUserWarehouse() + { + foreach ($this->tenantUser->warehouses as $warehouse) { + $warehouse->pivot->is_default = false; + $warehouse->pivot->save(); + } + } protected function unsetUserRole() { @@ -262,10 +271,10 @@ private function getDummyData($salesReturn = null) $approver = $invoice->form->requestApprovalTo; return [ - 'increment_group' => date('Ym'), - 'date' => date('Y-m-d H:i:s'), + 'increment_group' => '202212', + 'date' => '2022-12-12 12:17:07', 'sales_invoice_id' => $invoice->id, - "warehouse_id" => $this->warehouseSelected->id, + 'warehouse_id' => $this->warehouseSelected->id, 'customer_id' => $customer->id, 'customer_name' => $customer->name, 'customer_label' => $customer->code, @@ -273,22 +282,28 @@ private function getDummyData($salesReturn = null) 'customer_phone' => null, 'customer_email' => null, 'notes' => null, - 'tax' => 3000, - 'amount' => 33000, - 'type_of_tax' => 'exclude', + 'sub_total' => 30000, + 'tax_base' => 30000, + 'tax' => 2727.2727272727, + 'type_of_tax' => 'include', + 'amount' => 30000, 'items' => [ [ 'sales_invoice_item_id' => $invoiceItem->id, 'item_id' => $this->item->id, 'item_name' => $this->item->name, - 'item_label' => "[{$this->item->code}] - {$this->item->name}", + 'item_label' => '[{$this->item->code}] - {$this->item->name}', 'more' => false, 'unit' => $this->unit->label, + 'expiry_date' => null, + 'production_number' => null, 'converter' => $invoiceItem->converter, 'quantity_sales' => $quantityInvoice, + 'discount_percent' => null, + 'discount_value' => 0, 'quantity' => 3, - 'price' => $invoiceItem->price, - 'total' => 3 * $invoiceItem->price, + 'price' => 10000, + 'total' => 30000, 'allocation_id' => null, 'notes' => null, ], @@ -318,7 +333,7 @@ private function createSalesInvoice() 'delivery_fee' => 0, 'discount_percent' => 0, 'discount_value' => 0, - 'type_of_tax' => 'exclude', + 'type_of_tax' => 'include', 'tax' => 100000, 'amount' => 1100000, 'remaining' => 1100000, @@ -330,7 +345,7 @@ private function createSalesInvoice() 'item_referenceable_type' => 'SalesDeliveryNoteItem', 'item_id' => $this->item->id, 'item_name' => $this->item->name, - 'item_label' => "[{$this->item->code}] - {$this->item->name}", + 'item_label' => '[{$this->item->code}] - {$this->item->name}', 'more' => false, 'unit' => $this->unit->label, 'converter' => 1, @@ -376,20 +391,20 @@ private function createPaymentCollection($salesReturn) 'customer_email' => null, 'notes' => null, 'amount' => 30000, - "details" => [ + 'details' => [ [ - "date" => date("Y-m-d H:i:s"), - "chart_of_account_id" => null, - "chart_of_account_name" => null, - "available" => $salesReturn->amount, - "amount" => 30000, - "allocation_id" => null, - "allocation_name" => null, - "referenceable_form_date" => $salesReturn->form->date, - "referenceable_form_number" => $salesReturn->form->number, - "referenceable_form_notes" => $salesReturn->form->notes, - "referenceable_id" => $salesReturn->id, - "referenceable_type" => "SalesReturn" + 'date' => date('Y-m-d H:i:s'), + 'chart_of_account_id' => null, + 'chart_of_account_name' => null, + 'available' => $salesReturn->amount, + 'amount' => 30000, + 'allocation_id' => null, + 'allocation_name' => null, + 'referenceable_form_date' => $salesReturn->form->date, + 'referenceable_form_number' => $salesReturn->form->number, + 'referenceable_form_notes' => $salesReturn->form->notes, + 'referenceable_id' => $salesReturn->id, + 'referenceable_type' => 'SalesReturn' ], ], 'request_approval_to' => $this->approver->id, diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php index e5b199085..eef859027 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php @@ -4,8 +4,13 @@ use Tests\TestCase; +use App\Mail\Sales\SalesReturnApprovalRequest; use App\Model\Form; +use App\Model\SettingJournal; use App\Model\Sales\SalesReturn\SalesReturn; +use App\Model\Sales\SalesInvoice\SalesInvoice; +use Illuminate\Support\Facades\Mail; +use App\Helpers\Inventory\InventoryHelper; class SalesReturnTest extends TestCase { @@ -13,431 +18,1494 @@ class SalesReturnTest extends TestCase public static $path = '/api/v1/sales/return'; - /** @test */ - public function unauthorized_create_sales_return() - { + /** @test */ + public function unauthorized_no_default_branch_create_sales_return() + { + $this->setRole(); $data = $this->getDummyData(); - + + $this->branchDefault->pivot->is_default = false; + $this->branchDefault->pivot->save(); + $response = $this->json('POST', self::$path, $data, $this->headers); - + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'please set default branch to create this form' + ]); + } + + /** @test */ + public function unauthorized_create_sales_return() + { + $data = $this->getDummyData(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `create sales return` for guard `api`.' + ]); + } + + /** @test */ + public function invalid_data_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + $data = data_set($data, 'sales_invoice_id', null); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJsonFragment([ + 'code' => 422, + 'message' => 'The given data was invalid.' + ]); + } + + /** @test */ + public function duplicate_entry_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturn->form->number = 'SR22120002'; + $salesReturn->form->save(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + $response->assertStatus(400) ->assertJson([ - "code" => 0, - "message" => "There is no permission named `create sales return` for guard `api`." + 'code' => 400, + 'message' => 'Duplicate data entry' ]); - } - - /** @test */ - public function overquantity_create_sales_return() - { + } + + /** @test */ + public function error_sales_invoice_done_create_sales_return() + { $this->setRole(); - + $data = $this->getDummyData(); - $data = data_set($data, 'items.0.quantity', 100); - + + $salesInvoice = SalesInvoice::orderBy('id', 'asc')->first(); + $salesInvoice->form->done = 1; + $salesInvoice->form->save(); + $response = $this->json('POST', self::$path, $data, $this->headers); - + + $response->assertStatus(422) + ->assertJsonFragment([ + 'code' => 422, + 'message' => 'Sales return form already done' + ]); + } + + /** @test */ + public function error_notes_more_than_255_character_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + + $data = data_set($data, 'notes', $this->faker->text(500)); + $response = $this->json('POST', self::$path, $data, $this->headers); + $response->assertStatus(422) ->assertJson([ - "code" => 422, - "message" => "Sales return item can't exceed sales invoice qty" + 'code' => 422, + 'message' => 'The given data was invalid.', + 'errors' => [ + 'notes' => [ + 'The notes may not be greater than 255 characters.' + ] + ] ]); - } - - /** @test */ - public function invalid_create_sales_return() - { + } + + /** @test */ + public function whitespaces_trimmed_create_sales_return() + { $this->setRole(); - + $data = $this->getDummyData(); - $data = data_set($data, 'sales_invoice_id', null); - + + $data = data_set($data, 'notes', ' whitespaces trimmed '); $response = $this->json('POST', self::$path, $data, $this->headers); - + + $response->assertStatus(201) + ->assertJsonFragment([ + 'notes' => 'whitespaces trimmed' + ]); + } + + /** @test */ + public function overquantity_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + $data = data_set($data, 'items.0.quantity', 100); + $data = data_set($data, 'items.0.total', 1000000); + $data = data_set($data, 'sub_total', 1000000); + $data = data_set($data, 'tax_base', 1000000); + $data = data_set($data, 'tax', 90909.09090909091); + $data = data_set($data, 'amount', 1000000); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'Sales return item can\'t exceed sales invoice qty' + ]); + } + + /** @test */ + public function invalid_total_item_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + $data = data_set($data, 'items.0.total', 20000); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'total for item ' .$data['items'][0]['item_name']. ' should be 30000' + ]); + } + + /** @test */ + public function invalid_sub_total_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + $data = data_set($data, 'sub_total', 20000); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'sub total should be 30000' + ]); + } + + /** @test */ + public function invalid_tax_base_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + $data = data_set($data, 'tax_base', 20000); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'tax base should be 30000' + ]); + } + + /** @test */ + public function invalid_type_of_tax_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + $data = data_set($data, 'type_of_tax', 'exclude'); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'type of tax should be same with invoice' + ]); + } + + /** @test */ + public function invalid_tax_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + $data = data_set($data, 'tax', 3000); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'tax should be 2727.2727272727' + ]); + } + + /** @test */ + public function invalid_amount_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + $data = data_set($data, 'amount', 40000); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'amount should be 30000' + ]); + } + + /** @test */ + public function error_journal_not_found_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + + $settingJournal = SettingJournal::where('feature', 'sales')->where('name', 'account receivable')->first(); + $settingJournal->chart_of_account_id = null; + $settingJournal->save(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'Journal sales account - account receivable not found' + ]); + } + + /** @test */ + public function check_journal_balance_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $journal = SalesReturn::checkJournalBalance($salesReturn); + $this->assertEquals($journal['debit'], $journal['credit']); + } + + /** @test */ + public function success_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + + Mail::fake(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + Mail::assertQueued(SalesReturnApprovalRequest::class); + + $salesReturn = SalesReturn::where('id', $response->json('data.id'))->first(); + + $this->assertIsObject( + $salesReturn->salesInvoice(), + 'is sales invoice referenced', + ); + + $response->assertStatus(201) + ->assertJson([ + 'data' => [ + 'id' => $response->json('data.id'), + 'sales_invoice_id' => $data['sales_invoice_id'], + 'warehouse_id' => $data['warehouse_id'], + 'customer_id' => $data['customer_id'], + 'customer_name' => $data['customer_name'], + 'customer_address' => $data['customer_address'], + 'customer_phone' => $data['customer_phone'], + 'tax' => $data['tax'], + 'amount' => $data['amount'], + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.form.date'), + 'number' => 'SR22120001', + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $data['notes'], + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.form.updated_by'), + 'done' => 0, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $response->json('data.id'), + 'formable_type' => 'SalesReturn', + 'request_approval_at' => $response->json('data.form.request_approval_at'), + 'request_approval_to' => $data['request_approval_to'], + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, + 'approval_status' => 0, + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => $salesReturn->form->cancellation_status, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, + ], + 'items' => [ + [ + 'id' => $response->json('data.items.0.id'), + 'sales_return_id' => $response->json('data.id'), + 'sales_invoice_item_id' => $data['items'][0]['sales_invoice_item_id'], + 'item_id' => $data['items'][0]['item_id'], + 'item_name' => $data['items'][0]['item_name'], + 'quantity' => $data['items'][0]['quantity'], + 'quantity_sales' => $data['items'][0]['quantity_sales'], + 'price' => $data['items'][0]['price'], + 'discount_percent' => $data['items'][0]['discount_percent'], + 'discount_value' => $data['items'][0]['discount_value'] .'000000000000000000000000000000', + 'unit' => $data['items'][0]['unit'], + 'converter' => $data['items'][0]['converter'], + 'expiry_date' => $data['items'][0]['expiry_date'], + 'production_number' => $data['items'][0]['production_number'], + 'notes' => $data['items'][0]['notes'], + 'allocation_id' => $data['items'][0]['allocation_id'], + ] + ] + ] + ]); + + $this->assertDatabaseHas('forms', [ + 'id' => $response->json('data.form.id'), + 'number' => 'SR22120001', + 'notes' => $data['notes'], + 'created_by' => $response->json('data.form.created_by'), + 'updated_by' => $response->json('data.form.updated_by'), + 'approval_status' => 0, + 'done' => 0, + 'formable_id' => $response->json('data.id'), + 'formable_type' => 'SalesReturn', + 'request_approval_to' => $data['request_approval_to'], + ], 'tenant'); + + $this->assertDatabaseHas('sales_returns', [ + 'id' => $response->json('data.id'), + 'sales_invoice_id' => $data['sales_invoice_id'], + 'customer_id' => $data['customer_id'], + 'customer_name' => $data['customer_name'], + 'customer_address' => $data['customer_address'], + 'customer_phone' => $data['customer_phone'], + 'tax' => $data['tax'], + 'amount' => $data['amount'], + 'warehouse_id' => $data['warehouse_id'], + ], 'tenant'); + + $this->assertDatabaseHas('sales_return_items', [ + 'sales_return_id' => $response->json('data.id'), + 'sales_invoice_item_id' => $data['items'][0]['sales_invoice_item_id'], + 'item_id' => $data['items'][0]['item_id'], + 'item_name' => $data['items'][0]['item_name'], + 'quantity' => $data['items'][0]['quantity'], + 'quantity_sales' => $data['items'][0]['quantity_sales'], + 'price' => $data['items'][0]['price'], + 'discount_percent' => $data['items'][0]['discount_percent'], + 'discount_value' => $data['items'][0]['discount_value'] .'000000000000000000000000000000', + 'unit' => $data['items'][0]['unit'], + 'converter' => $data['items'][0]['converter'], + 'expiry_date' => $data['items'][0]['expiry_date'], + 'production_number' => $data['items'][0]['production_number'], + 'notes' => $data['items'][0]['notes'], + 'allocation_id' => $data['items'][0]['allocation_id'], + ], 'tenant'); + } + + /** @test */ + public function unauthorized_no_branch_read_all_sales_return() + { + $this->success_create_sales_return(); + + $this->branchDefault->pivot->is_default = false; + $this->branchDefault->pivot->save(); + + $data = [ + 'join' => 'form,customer,items,item', + 'fields' => 'sales_return.*', + 'sort_by' => '-form.number', + 'group_by' => 'form.id', + 'filter_form' => 'notArchived', + 'filter_like' => '{}', + 'limit' => 10, + 'includes' => 'customer;warehouse;items.item;items.allocation;salesInvoice.form;form.createdBy;form.requestApprovalTo;form.branch', + 'page' => 1 + ]; + + $response = $this->json('GET', self::$path, $data, $this->headers); + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'please set default branch to read this form' + ]); + } + + /** @test */ + public function unauthorized_no_branch_read_sales_return() + { + $this->success_create_sales_return(); + + $this->branchDefault->pivot->is_default = false; + $this->branchDefault->pivot->save(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $data = [ + 'with_archives' => 'true', + 'with_origin' => 'true', + 'remaining_info' => 'true', + 'includes' => 'customer;warehouse;items.item;items.allocation;salesInvoice.form;form.createdBy;form.requestApprovalTo;form.branch' + ]; + + $response = $this->json('GET', self::$path . '/' . $salesReturn->id, $data, $this->headers); + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'please set default branch to read this form' + ]); + } + + /** @test */ + public function unauthorized_read_all_sales_return() + { + $this->success_create_sales_return(); + + $this->unsetUserRole(); + + $data = [ + 'join' => 'form,customer,items,item', + 'fields' => 'sales_return.*', + 'sort_by' => '-form.number', + 'group_by' => 'form.id', + 'filter_form' => 'notArchived', + 'filter_like' => '{}', + 'limit' => 10, + 'includes' => 'customer;warehouse;items.item;items.allocation;salesInvoice.form;form.createdBy;form.requestApprovalTo;form.branch', + 'page' => 1 + ]; + + $response = $this->json('GET', self::$path, $data, $this->headers); + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `read sales return` for guard `api`.' + ]); + } + + /** @test */ + public function unauthorized_read_sales_return() + { + $this->success_create_sales_return(); + + $this->unsetUserRole(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $data = [ + 'with_archives' => 'true', + 'with_origin' => 'true', + 'remaining_info' => 'true', + 'includes' => 'customer;warehouse;items.item;items.allocation;salesInvoice.form;form.createdBy;form.requestApprovalTo;form.branch' + ]; + + $response = $this->json('GET', self::$path . '/' . $salesReturn->id, $data, $this->headers); + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `read sales return` for guard `api`.' + ]); + } + + /** @test */ + public function success_read_all_sales_return() + { + $this->success_create_sales_return(); + + $data = [ + 'join' => 'form,customer,items,item', + 'fields' => 'sales_return.*', + 'sort_by' => '-form.number', + 'group_by' => 'form.id', + 'filter_form' => 'notArchived', + 'filter_like' => '{}', + 'limit' => 10, + 'includes' => 'form;customer;items.item;items.allocation', + 'page' => 1 + ]; + + $response = $this->json('GET', self::$path, $data, $this->headers); + $response->assertStatus(200) + ->assertJsonStructure([ + 'data' => [ + [ + 'id', + 'sales_invoice_id', + 'customer_id', + 'warehouse_id', + 'customer_name', + 'customer_address', + 'customer_phone', + 'tax', + 'amount', + 'form' => [ + 'id', + 'date', + 'number', + 'edited_number', + 'edited_notes', + 'notes', + 'created_by', + 'updated_by', + 'done', + 'increment', + 'increment_group', + 'formable_id', + 'formable_type', + 'request_approval_at', + 'request_approval_to', + 'approval_by', + 'approval_at', + 'approval_reason', + 'approval_status', + 'request_cancellation_to', + 'request_cancellation_by', + 'request_cancellation_at', + 'request_cancellation_reason', + 'cancellation_approval_at', + 'cancellation_approval_by', + 'cancellation_approval_reason', + 'cancellation_status', + 'request_close_to', + 'request_close_by', + 'request_close_at', + 'request_close_reason', + 'close_approval_at', + 'close_approval_by', + 'close_status' + ], + 'customer' => [ + 'id', + 'code', + 'tax_identification_number', + 'name', + 'address', + 'city', + 'state', + 'country', + 'zip_code', + 'latitude', + 'longitude', + 'phone', + 'phone_cc', + 'email', + 'notes', + 'credit_limit', + 'branch_id', + 'created_by', + 'updated_by', + 'archived_by', + 'pricing_group_id', + 'label' + ], + 'items' => [ + [ + 'id', + 'sales_return_id', + 'sales_invoice_item_id', + 'item_id', + 'item_name', + 'quantity', + 'quantity_sales', + 'price', + 'discount_percent', + 'discount_value', + 'unit', + 'converter', + 'expiry_date', + 'production_number', + 'notes', + 'allocation_id', + 'item' => [ + 'id', + 'chart_of_account_id', + 'code', + 'barcode', + 'name', + 'size', + 'color', + 'weight', + 'notes', + 'taxable', + 'require_production_number', + 'require_expiry_date', + 'stock', + 'stock_reminder', + 'unit_default', + 'unit_default_purchase', + 'unit_default_sales', + 'label' + ] + ] + ] + ] + ], + 'links' => [ + 'first', + 'last', + 'prev', + 'next', + ], + 'meta' => [ + 'current_page', + 'from', + 'last_page', + 'path', + 'per_page', + 'to', + 'total', + ] + ]); + $this->assertGreaterThan(0, count($response->json('data'))); + } + + /** @test */ + public function read_sales_return() + { + $this->success_create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturnItem = $salesReturn->items[0]; + $salesReturnForm = $salesReturn->form; + + $data = [ + 'with_archives' => 'true', + 'with_origin' => 'true', + 'remaining_info' => 'true', + 'includes' => 'customer;items.item;items.allocation;salesInvoice.form;form.createdBy;form.requestApprovalTo;form.branch' + ]; + + $response = $this->json('GET', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + 'id' => $salesReturn->id, + 'sales_invoice_id' => $salesReturn->sales_invoice_id, + 'warehouse_id' => $salesReturn->warehouse_id, + 'customer_id' => $salesReturn->customer_id, + 'customer_name' => $salesReturn->customer_name, + 'customer_address' => $salesReturn->customer_address, + 'customer_phone' => $salesReturn->customer_phone, + 'tax' => $salesReturn->tax, + 'amount' => $salesReturn->amount, + 'archives' => [], + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.form.date'), + 'number' => $salesReturnForm->number, + 'edited_number' => $salesReturnForm->edited_number, + 'edited_notes' => $salesReturnForm->edited_notes, + 'notes' => $salesReturnForm->notes, + 'created_by' => $salesReturnForm->created_by, + 'updated_by' => $response->json('data.form.updated_by'), + 'done' => $salesReturnForm->done, + 'increment' => $salesReturnForm->increment, + 'increment_group' => $salesReturnForm->increment_group, + 'formable_id' => $salesReturnForm->formable_id, + 'formable_type' => $salesReturnForm->formable_type, + 'request_approval_at' => $response->json('data.form.request_approval_at'), + 'request_approval_to' => $salesReturnForm->request_approval_to, + 'approval_by' => $salesReturnForm->approval_by, + 'approval_at' => $response->json('data.form.approval_at'), + 'approval_reason' => $salesReturnForm->approval_reason, + 'approval_status' => $salesReturnForm->approval_status, + 'request_cancellation_to' => $salesReturnForm->request_cancellation_to, + 'request_cancellation_by' => $salesReturnForm->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturnForm->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturnForm->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturnForm->cancellation_approval_reason, + 'cancellation_status' => $salesReturnForm->cancellation_status, + 'request_close_to' => $salesReturnForm->request_close_to, + 'request_close_by' => $salesReturnForm->request_close_by, + 'request_close_at' => $response->json('data.form.request_close_at'), + 'request_close_reason' => $salesReturnForm->request_close_reason, + 'close_approval_at' => $response->json('data.form.close_approval_at'), + 'close_approval_by' => $salesReturnForm->close_approval_by, + 'close_status' => $salesReturnForm->close_status, + 'created_by' => [ + 'id' => $salesReturnForm->createdBy->id, + 'name' => $salesReturnForm->createdBy->name, + 'first_name' => $salesReturnForm->createdBy->first_name, + 'last_name' => $salesReturnForm->createdBy->last_name, + 'address' => $salesReturnForm->createdBy->address, + 'phone' => $salesReturnForm->createdBy->phone, + 'email' => $salesReturnForm->createdBy->email, + 'branch_id' => $salesReturnForm->createdBy->branch_id, + 'warehouse_id' => $salesReturnForm->createdBy->warehouse_id, + 'full_name' => $salesReturnForm->createdBy->full_name + ], + 'request_approval_to' => [ + 'id' => $salesReturnForm->requestApprovalTo->id, + 'name' => $salesReturnForm->requestApprovalTo->name, + 'first_name' => $salesReturnForm->requestApprovalTo->first_name, + 'last_name' => $salesReturnForm->requestApprovalTo->last_name, + 'address' => $salesReturnForm->requestApprovalTo->address, + 'phone' => $salesReturnForm->requestApprovalTo->phone, + 'email' => $salesReturnForm->requestApprovalTo->email, + 'branch_id' => $salesReturnForm->requestApprovalTo->branch_id, + 'warehouse_id' => $salesReturnForm->requestApprovalTo->warehouse_id, + 'full_name' => $salesReturnForm->requestApprovalTo->full_name + ], + 'branch' => [ + 'id' => $salesReturnForm->branch->id, + 'name' => $salesReturnForm->branch->name, + 'address' => $salesReturnForm->branch->address, + 'phone' => $salesReturnForm->branch->phone, + 'archived_at' => $salesReturnForm->branch->archived_at, + ] + ], + 'items' => [ + [ + 'id' => $salesReturnItem->id, + 'sales_return_id' => $salesReturnItem->sales_return_id, + 'sales_invoice_item_id' => $salesReturnItem->sales_invoice_item_id, + 'item_id' => $salesReturnItem->item_id, + 'item_name' => $salesReturnItem->item_name, + 'quantity' => $salesReturnItem->quantity, + 'quantity_sales' => $salesReturnItem->quantity_sales, + 'price' => $salesReturnItem->price, + 'discount_percent' => $salesReturnItem->discount_percent, + 'discount_value' => $salesReturnItem->discount_value, + 'unit' => $salesReturnItem->unit, + 'converter' => $salesReturnItem->converter, + 'expiry_date' => $salesReturnItem->expiry_date, + 'production_number' => $salesReturnItem->production_number, + 'notes' => $salesReturnItem->notes, + 'allocation_id' => $salesReturnItem->allocation_id, + 'item' => [ + 'id' => $salesReturnItem->item->id, + 'chart_of_account_id' => $salesReturnItem->item->chart_of_account_id, + 'code' => $salesReturnItem->item->code, + 'barcode' => $salesReturnItem->item->barcode, + 'name' => $salesReturnItem->item->name, + 'size' => $salesReturnItem->item->size, + 'color' => $salesReturnItem->item->color, + 'weight' => $salesReturnItem->item->weight, + 'notes' => $salesReturnItem->item->notes, + 'taxable' => $salesReturnItem->item->taxable, + 'require_production_number' => $salesReturnItem->item->require_production_number, + 'require_expiry_date' => $salesReturnItem->item->require_expiry_date, + 'stock' => $salesReturnItem->item->stock, + 'stock_reminder' => $salesReturnItem->item->stock_reminder, + 'unit_default' => $salesReturnItem->item->unit_default, + 'unit_default_purchase' => $salesReturnItem->item->unit_default_purchase, + 'unit_default_sales' => $salesReturnItem->item->unit_default_sales, + 'label' => $salesReturnItem->item->label, + ], + 'allocation' => null + ] + ], + 'sales_invoice' => [ + 'id' => $salesReturn->salesInvoice->id, + 'customer_id' => $salesReturn->salesInvoice->customer_id, + 'customer_name' => $salesReturn->salesInvoice->customer_name, + 'customer_address' => $salesReturn->salesInvoice->customer_address, + 'customer_phone' => $salesReturn->salesInvoice->customer_phone, + 'discount_percent' => $salesReturn->salesInvoice->discount_percent, + 'discount_value' => $salesReturn->salesInvoice->discount_value, + 'type_of_tax' => $salesReturn->salesInvoice->type_of_tax, + 'tax' => $salesReturn->salesInvoice->tax, + 'amount' => $salesReturn->salesInvoice->amount, + 'remaining' => $salesReturn->salesInvoice->remaining, + 'form' => [ + 'id' => $salesReturn->salesInvoice->form->id, + 'date' => $response->json('data.sales_invoice.form.date'), + 'number' => $salesReturn->salesInvoice->form->number, + 'edited_number' => $salesReturn->salesInvoice->form->edited_number, + 'edited_notes' => $salesReturn->salesInvoice->form->edited_notes, + 'notes' => $salesReturn->salesInvoice->form->notes, + 'created_by' => $salesReturn->salesInvoice->form->created_by, + 'updated_by' => $response->json('data.sales_invoice.form.updated_by'), + 'done' => $salesReturn->salesInvoice->form->done, + 'increment' => $salesReturn->salesInvoice->form->increment, + 'increment_group' => $salesReturn->salesInvoice->form->increment_group, + 'formable_id' => $salesReturn->salesInvoice->form->formable_id, + 'formable_type' => $salesReturn->salesInvoice->form->formable_type, + 'request_approval_at' => $response->json('data.sales_invoice.form.request_approval_at'), + 'request_approval_to' => $salesReturn->salesInvoice->form->request_approval_to, + 'approval_by' => $salesReturn->salesInvoice->form->approval_by, + 'approval_at' => $response->json('data.sales_invoice.form.approval_at'), + 'approval_reason' => $salesReturn->salesInvoice->form->approval_reason, + 'approval_status' => $salesReturn->salesInvoice->form->approval_status, + 'request_cancellation_to' => $salesReturn->salesInvoice->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->salesInvoice->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.sales_invoice.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->salesInvoice->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.sales_invoice.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->salesInvoice->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->salesInvoice->form->cancellation_approval_reason, + 'cancellation_status' => $salesReturn->salesInvoice->form->cancellation_status, + 'request_close_to' => $salesReturn->salesInvoice->form->request_close_to, + 'request_close_by' => $salesReturn->salesInvoice->form->request_close_by, + 'request_close_at' => $response->json('data.sales_invoice.form.request_close_at'), + 'request_close_reason' => $salesReturn->salesInvoice->form->request_close_reason, + 'close_approval_at' => $response->json('data.sales_invoice.form.close_approval_at'), + 'close_approval_by' => $salesReturn->salesInvoice->form->close_approval_by, + 'close_status' => $salesReturn->salesInvoice->form->close_status, + ] + ] + ] + ]); + } + + /** @test */ + public function unauthorized_no_default_branch_update_sales_return() + { + $this->success_create_sales_return(); + + $this->branchDefault->pivot->is_default = false; + $this->branchDefault->pivot->save(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data = $this->getDummyData($salesReturn); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'please set default branch to update this form' + ]); + } + + /** @test */ + public function referenced_update_sales_return() + { + $this->success_create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $this->createPaymentCollection($salesReturn); + + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form referenced by payment collection' + ]); + } + + /** @test */ + public function unauthorized_update_sales_return() + { + $this->success_create_sales_return(); + + $this->unsetUserRole(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data = $this->getDummyData($salesReturn); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(500) + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `update sales return` for guard `api`.' + ]); + } + + /** @test */ + public function invalid_data_update_sales_return() + { + $this->success_create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + $data = data_set($data, 'sales_invoice_id', null); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.' + ]); + } + + /** @test */ + public function error_form_done_update_sales_return() + { + $this->success_create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturn->form->done = 1; + $salesReturn->form->save(); + + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form already done' + ]); + } + + /** @test */ + public function error_notes_more_than_255_character_update_sales_return() + { + $this->success_create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + $data = data_set($data, 'notes', $this->faker->text(500)); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + 'errors' => [ + 'notes' => [ + 'The notes may not be greater than 255 characters.' + ] + ] + ]); + } + + /** @test */ + public function whitespaces_trimmed_update_sales_return() + { + $this->success_create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + $data = data_set($data, 'notes', ' whitespaces trimmed '); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(201) + ->assertJsonFragment([ + 'notes' => 'whitespaces trimmed' + ]); + } + + /** @test */ + public function overquantity_update_sales_return() + { + $this->success_create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + $data = data_set($data, 'items.0.quantity', 100); + $data = data_set($data, 'items.0.total', 1000000); + $data = data_set($data, 'sub_total', 1000000); + $data = data_set($data, 'tax_base', 1000000); + $data = data_set($data, 'tax', 90909.09090909091); + $data = data_set($data, 'amount', 1000000); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + $response->assertStatus(422) ->assertJson([ - "code" => 422, - "message" => "The given data was invalid." + 'code' => 422, + 'message' => 'Sales return item can\'t exceed sales invoice qty' ]); - } + } + + /** @test */ + public function invalid_total_item_update_sales_return() + { + $this->success_create_sales_return(); - /** @test */ - public function success_create_sales_return() - { - $this->setRole(); - - $data = $this->getDummyData(); - - $response = $this->json('POST', self::$path, $data, $this->headers); - - $response->assertStatus(201) - ->assertJson([ - "data" => [ - "id" => $response->json('data.id'), - "form" => [ - "id" => $response->json('data.form.id'), - "date" => $response->json('data.form.date'), - "number" => $response->json('data.form.number'), - "notes" => $response->json('data.form.notes'), - ] - ] + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + $data = data_set($data, 'items.0.total', 20000); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'total for item ' .$data['items'][0]['item_name']. ' should be 30000' ]); - - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_status' => 0, - 'done' => 0, - ], 'tenant'); - - $this->assertDatabaseHas('sales_returns', [ - 'id' => $response->json('data.id'), - 'tax' => $response->json('data.tax'), - 'customer_id' => $response->json('data.customer_id'), - 'amount' => $response->json('data.amount'), - ], 'tenant'); - - $this->assertDatabaseHas('sales_return_items', [ - 'sales_return_id' => $response->json('data.id'), - 'item_id' => $response->json('data.items.0.item_id'), - 'quantity' => $response->json('data.items.0.quantity'), - ], 'tenant'); - } - - /** @test */ - public function success_approve_sales_return() - { + } + + /** @test */ + public function invalid_sub_total_update_sales_return() + { $this->success_create_sales_return(); - + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); - - $response->assertStatus(200) + + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + $data = data_set($data, 'sub_total', 20000); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) ->assertJson([ - "data" => [ - "id" => $salesReturn->id, - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - "approval_status" => 1, - ] - ] + 'code' => 422, + 'message' => 'sub total should be 30000' ]); - - $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_by' => $response->json('data.form.approval_by'), - 'approval_status' => 1, - ], 'tenant'); - $this->assertDatabaseHas('inventories', [ - 'form_id' => $response->json('data.form.id'), - 'item_id' => $response->json('data.items.0.item_id'), - 'quantity' => $response->json('data.items.0.quantity'), - ], 'tenant'); - } - - /** @test */ - public function read_all_sales_return() - { + } + + /** @test */ + public function invalid_tax_base_update_sales_return() + { $this->success_create_sales_return(); - - $data = [ - 'join' => 'form,customer,items,item', - 'fields' => 'sales_return.*', - 'sort_by' => '-form.number', - 'group_by' => 'form.id', - 'filter_form' => 'notArchived', - 'filter_like' => '{}', - 'limit' => 10, - 'includes' => 'customer;warehouse;items.item;items.allocation;salesInvoice.form;form.createdBy;form.requestApprovalTo;form.branch', - 'page' => 1 - ]; - - $response = $this->json('GET', self::$path, $data, $this->headers); - $response->assertStatus(200) - ->assertJsonStructure([ - "data" => [ - [ - "id", - "tax", - "amount", - "warehouse" => [ - "id", - "name", - ], - "sales_invoice" => [ - "form" => [ - "number", - ] - ], - "form" => [ - "id", - "date", - "number", - "notes", - ], - "items" => [ - [ - "id", - "item_id", - "item_name", - "quantity", - "quantity_sales", - "price", - "discount_value", - "unit", - "converter", - "allocation" - ] - ] - ] - ] - ]); - $this->assertGreaterThan(0, count($response->json('data'))); - } - - /** @test */ - public function read_sales_return() - { - $this->success_approve_sales_return(); - + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $data = [ - 'with_archives' => 'true', - 'with_origin' => 'true', - 'remaining_info' => 'true', - 'includes' => 'customer;warehouse;items.item;items.allocation;salesInvoice.form;form.createdBy;form.requestApprovalTo;form.branch' - ]; - - $response = $this->json('GET', self::$path . '/' . $salesReturn->id, $data, $this->headers); - - $response->assertStatus(200) + + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + $data = data_set($data, 'tax_base', 20000); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) ->assertJson([ - "data" => [ - "id" => $salesReturn->id, - "tax" => $salesReturn->tax, - "amount" => $salesReturn->amount, - "warehouse" => [ - "id" => $salesReturn->warehouse->id, - "name" => $salesReturn->warehouse->name, - ], - "sales_invoice" => [ - "form" => [ - "number" => $salesReturn->salesInvoice->form->number, - ] - ], - "form" => [ - "id" => $salesReturn->form->id, - "date" => $salesReturn->form->date, - "number" => $salesReturn->form->number, - "notes" => $salesReturn->form->notes, - ], - "items" => [ - [ - "id" => $salesReturn->items[0]->id, - "item_id" => $salesReturn->items[0]->item->id, - "item_name" => $salesReturn->items[0]->item->name, - "quantity" => $salesReturn->items[0]->quantity, - "quantity_sales" => $salesReturn->items[0]->quantity_sales, - "price" => $salesReturn->items[0]->price, - "discount_value" => $salesReturn->items[0]->discount_value, - "unit" => $salesReturn->items[0]->unit, - "converter" => $salesReturn->items[0]->converter, - "allocation" => null - ] - ] - ] + 'code' => 422, + 'message' => 'tax base should be 30000' ]); - } - - /** @test */ - public function unauthorized_update_sales_return() - { + } + + /** @test */ + public function invalid_type_of_tax_update_sales_return() + { $this->success_create_sales_return(); - - $this->unsetUserRole(); - + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data = $this->getDummyData($salesReturn); - + $data = data_set($data, 'id', $salesReturn->id, false); + $data = data_set($data, 'type_of_tax', 'exclude'); + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); - - $response->assertStatus(500) - ->assertJson([ - "code" => 0, - "message" => "There is no permission named `update sales return` for guard `api`." - ]); - } - - /** @test */ - public function referenced_update_sales_return() - { + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'type of tax should be same with invoice' + ]); + } + + /** @test */ + public function invalid_tax_update_sales_return() + { $this->success_create_sales_return(); - + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); - - $this->createPaymentCollection($salesReturn); $data = $this->getDummyData($salesReturn); $data = data_set($data, 'id', $salesReturn->id, false); - + $data = data_set($data, 'tax', 3000); + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); - - $response - ->assertStatus(422) - ->assertJsonFragment(['message' => 'Cannot edit form because referenced by payment collection']); - } - - /** @test */ - public function overquantity_update_sales_return() - { + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'tax should be 2727.2727272727' + ]); + } + + /** @test */ + public function invalid_amount_update_sales_return() + { $this->success_create_sales_return(); - + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data = $this->getDummyData($salesReturn); $data = data_set($data, 'id', $salesReturn->id, false); - $data = data_set($data, 'items.0.quantity', 100); - + $data = data_set($data, 'amount', 40000); + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); - + $response->assertStatus(422) - ->assertJson([ - "code" => 422, - "message" => "Sales return item can't exceed sales invoice qty" - ]); - } - - /** @test */ - public function invalid_update_sales_return() - { + ->assertJson([ + 'code' => 422, + 'message' => 'amount should be 30000' + ]); + } + + /** @test */ + public function error_journal_not_found_update_sales_return() + { $this->success_create_sales_return(); - + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data = $this->getDummyData($salesReturn); $data = data_set($data, 'id', $salesReturn->id, false); - $data = data_set($data, 'sales_invoice_id', null); - + + $settingJournal = SettingJournal::where('feature', 'sales')->where('name', 'account receivable')->first(); + $settingJournal->chart_of_account_id = null; + $settingJournal->save(); + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); - + $response->assertStatus(422) - ->assertJson([ - "code" => 422, - "message" => "The given data was invalid." - ]); - } - - /** @test */ - public function success_update_sales_return() - { + ->assertJson([ + 'code' => 422, + 'message' => 'Journal sales account - account receivable not found' + ]); + } + + /** @test */ + public function check_journal_balance_update_sales_return() + { $this->success_create_sales_return(); - + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data = $this->getDummyData($salesReturn); $data = data_set($data, 'id', $salesReturn->id, false); - + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); - + + $salesReturn = SalesReturn::where('id', $response->json('data.id'))->first(); + $journal = SalesReturn::checkJournalBalance($salesReturn); + $this->assertEquals($journal['debit'], $journal['credit']); + } + + /** @test */ + public function success_update_sales_return() + { + $this->success_create_sales_return(); + + $oldSalesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($oldSalesReturn); + $data = data_set($data, 'id', $oldSalesReturn->id, false); + + Mail::fake(); + + $response = $this->json('PATCH', self::$path . '/' . $oldSalesReturn->id, $data, $this->headers); + + Mail::assertQueued(SalesReturnApprovalRequest::class); + + $salesReturn = SalesReturn::where('id', $response->json('data.id'))->first(); + + $this->assertIsObject( + $salesReturn->salesInvoice(), + 'is sales invoice referenced', + ); + $response->assertStatus(201) - ->assertJson([ - "data" => [ - "id" => $response->json('data.id'), - "form" => [ - "id" => $response->json('data.form.id'), - "date" => $response->json('data.form.date'), - "number" => $response->json('data.form.number'), - "notes" => $response->json('data.form.notes'), + ->assertJson([ + 'data' => [ + 'id' => $response->json('data.id'), + 'sales_invoice_id' => $data['sales_invoice_id'], + 'warehouse_id' => $data['warehouse_id'], + 'customer_id' => $data['customer_id'], + 'customer_name' => $data['customer_name'], + 'customer_address' => $data['customer_address'], + 'customer_phone' => $data['customer_phone'], + 'tax' => $data['tax'], + 'amount' => $data['amount'], + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.form.date'), + 'number' => 'SR22120001', + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $data['notes'], + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.form.updated_by'), + 'done' => 0, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $response->json('data.id'), + 'formable_type' => 'SalesReturn', + 'request_approval_at' => $response->json('data.form.request_approval_at'), + 'request_approval_to' => $data['request_approval_to'], + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, + 'approval_status' => 0, + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => $salesReturn->form->cancellation_status, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, + ], + 'items' => [ + [ + 'id' => $response->json('data.items.0.id'), + 'sales_return_id' => $response->json('data.id'), + 'sales_invoice_item_id' => $data['items'][0]['sales_invoice_item_id'], + 'item_id' => $data['items'][0]['item_id'], + 'item_name' => $data['items'][0]['item_name'], + 'quantity' => $data['items'][0]['quantity'], + 'quantity_sales' => $data['items'][0]['quantity_sales'], + 'price' => $data['items'][0]['price'], + 'discount_percent' => $data['items'][0]['discount_percent'], + 'discount_value' => $data['items'][0]['discount_value'] .'000000000000000000000000000000', + 'unit' => $data['items'][0]['unit'], + 'converter' => $data['items'][0]['converter'], + 'expiry_date' => $data['items'][0]['expiry_date'], + 'production_number' => $data['items'][0]['production_number'], + 'notes' => $data['items'][0]['notes'], + 'allocation_id' => $data['items'][0]['allocation_id'], ] + ] ] - ]); - - $this->assertDatabaseHas('forms', [ 'edited_number' => $response->json('data.form.number') ], 'tenant'); + ]); + + $this->assertDatabaseHas('forms', [ + 'id' => $oldSalesReturn->form->id, + 'edited_number' => $oldSalesReturn->form->edited_number, + 'formable_id' => $oldSalesReturn->id, + 'formable_type' => 'SalesReturn', + ], 'tenant'); $this->assertDatabaseHas('user_activities', [ 'number' => $response->json('data.form.number'), 'table_id' => $response->json('data.id'), 'table_type' => 'SalesReturn', 'activity' => 'Update - 1' ], 'tenant'); - + $this->assertDatabaseHas('forms', [ - 'id' => $response->json('data.form.id'), - 'number' => $response->json('data.form.number'), - 'approval_status' => 0, - 'done' => 0, + 'id' => $response->json('data.form.id'), + 'number' => $oldSalesReturn->form->edited_number, + 'notes' => $data['notes'], + 'created_by' => $response->json('data.form.created_by'), + 'updated_by' => $response->json('data.form.updated_by'), + 'approval_status' => 0, + 'done' => 0, + 'formable_id' => $response->json('data.id'), + 'formable_type' => 'SalesReturn', + 'request_approval_to' => $data['request_approval_to'], ], 'tenant'); - + $this->assertDatabaseHas('sales_returns', [ 'id' => $response->json('data.id'), - 'tax' => $response->json('data.tax'), - 'customer_id' => $response->json('data.customer_id'), - 'amount' => $response->json('data.amount'), + 'sales_invoice_id' => $data['sales_invoice_id'], + 'customer_id' => $data['customer_id'], + 'customer_name' => $data['customer_name'], + 'customer_address' => $data['customer_address'], + 'customer_phone' => $data['customer_phone'], + 'tax' => $data['tax'], + 'amount' => $data['amount'], + 'warehouse_id' => $data['warehouse_id'], ], 'tenant'); - + $this->assertDatabaseHas('sales_return_items', [ 'sales_return_id' => $response->json('data.id'), - 'item_id' => $response->json('data.items.0.item_id'), - 'quantity' => $response->json('data.items.0.quantity'), + 'sales_invoice_item_id' => $data['items'][0]['sales_invoice_item_id'], + 'item_id' => $data['items'][0]['item_id'], + 'item_name' => $data['items'][0]['item_name'], + 'quantity' => $data['items'][0]['quantity'], + 'quantity_sales' => $data['items'][0]['quantity_sales'], + 'price' => $data['items'][0]['price'], + 'discount_percent' => $data['items'][0]['discount_percent'], + 'discount_value' => $data['items'][0]['discount_value'] .'000000000000000000000000000000', + 'unit' => $data['items'][0]['unit'], + 'converter' => $data['items'][0]['converter'], + 'expiry_date' => $data['items'][0]['expiry_date'], + 'production_number' => $data['items'][0]['production_number'], + 'notes' => $data['items'][0]['notes'], + 'allocation_id' => $data['items'][0]['allocation_id'], ], 'tenant'); - } - - /** @test */ - public function unauthorized_delete_sales_return() - { + } + + /** @test */ + public function unauthorized_different_default_branch_delete_sales_return() + { $this->success_create_sales_return(); - + + $branch = $this->createBranch(); + $this->branchDefault->pivot->branch_id = $branch->id; + $this->branchDefault->pivot->is_default = true; + $this->branchDefault->pivot->save(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(200); + + $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'please set default branch to delete this form' + ]); + } + + /** @test */ + public function unauthorized_no_warehouse_default_branch_delete_sales_return() + { + $this->success_create_sales_return(); + + $this->removeUserWarehouse(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(200); + + $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'please set default warehouse to delete this form' + ]); + } + + /** @test */ + public function unauthorized_delete_sales_return() + { + $this->success_create_sales_return(); + $this->unsetUserRole(); - + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data['reason'] = $this->faker->text(200); - + $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); - + $response->assertStatus(500) - ->assertJson([ - "code" => 0, - "message" => "There is no permission named `delete sales return` for guard `api`." - ]); - } - - /** @test */ - public function referenced_delete_sales_return() - { + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `delete sales return` for guard `api`.' + ]); + } + /** @test */ + public function error_form_done_delete_sales_return() + { $this->success_create_sales_return(); - + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturn->form->done = 1; + $salesReturn->form->save(); + + $data['reason'] = $this->faker->text(200); + + $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form already done' + ]); + } + + /** @test */ + public function referenced_delete_sales_return() + { + $this->success_create_sales_return(); + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $this->createPaymentCollection($salesReturn); + $data['reason'] = $this->faker->text(200); - + $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); - - $response - ->assertStatus(422) - ->assertJsonFragment(['message' => 'Cannot edit form because referenced by payment collection']); - } - - /** @test */ - public function success_delete_sales_return() - { + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form referenced by payment collection' + ]); + } + + /** @test */ + public function error_empty_reason_delete_sales_return() + { $this->success_create_sales_return(); - + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, [], $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'The given data was invalid.', + 'errors' => [ + 'reason' => [ + 'The reason field is required.' + ] + ] + ]); + } + + /** @test */ + public function success_delete_sales_return() + { + $this->success_create_sales_return(); + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data['reason'] = $this->faker->text(200); - + + Mail::fake(); + $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); - + $response->assertStatus(204); + Mail::assertQueued(SalesReturnApprovalRequest::class); + $this->assertDatabaseHas('forms', [ - 'number' => $salesReturn->form->number, - 'request_cancellation_reason' => $data['reason'], - 'cancellation_status' => 0, + 'number' => $salesReturn->form->number, + 'request_cancellation_reason' => $data['reason'], + 'cancellation_status' => 0, ], 'tenant'); - } + } } \ No newline at end of file From c47cc2cbe2d4131e948492e5742ca95c026fa803 Mon Sep 17 00:00:00 2001 From: "@gunadi" Date: Fri, 16 Dec 2022 15:04:50 +0800 Subject: [PATCH 13/21] sales return: update tdd --- .../SalesReturnApprovalByEmailController.php | 4 +- .../SalesReturnApprovalByEmailTest.php | 213 ++++++++++++++++++ .../SalesReturn/SalesReturnApprovalTest.php | 139 +++++++++++- .../SalesReturnCancellationApprovalTest.php | 21 ++ .../SalesReturn/SalesReturnHistoryTest.php | 53 +++-- .../Sales/SalesReturn/SalesReturnTest.php | 97 +++++--- 6 files changed, 476 insertions(+), 51 deletions(-) diff --git a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php index efbfe8074..464ec5846 100644 --- a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php +++ b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php @@ -13,6 +13,8 @@ use App\Model\Sales\SalesReturn\SalesReturn; use Exception; use App\Model\Sales\SalesInvoice\SalesInvoiceReference; +use App\Model\Accounting\Journal; +use App\Model\Inventory\Inventory; class SalesReturnApprovalByEmailController extends Controller { @@ -42,7 +44,7 @@ public function approve(Request $request) foreach ($salesReturns as $salesReturn) { try { - if ($salesReturn->form->approval_status === 1) { + if ($salesReturn->form->approval_status === 1 && $salesReturn->form->cancellation_status === null) { throw new Exception('form '.$salesReturn->form->number.' already approved', 422); } } catch (\Throwable $th) { diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php index 8033609cb..89c3c9057 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php @@ -76,6 +76,16 @@ public function approve_sales_return() $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); } + public function delete_approved_sales_return() + { + $this->approve_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $data['reason'] = $this->faker->text(200); + + $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); + } + /** @test */ public function error_already_approved_approve_by_email_sales_return() { @@ -257,6 +267,118 @@ public function success_approve_by_email_sales_return() ] ]); } + + /** @test */ + public function success_approve_delete_by_email_sales_return() + { + $this->delete_approved_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturnItem = $salesReturn->items[0]; + + $approver = $salesReturn->form->requestCancellationTo; + $approverToken = $this->findOrCreateToken($approver); + + $this->changeActingAs($approver, $salesReturn); + + $data = [ + 'action' => 'approve', + 'approver_id' => $salesReturn->form->request_cancellation_to, + 'token' => $approverToken->token, + 'resource-type' => 'SalesReturn', + 'ids' => [ + ['id' => $salesReturn->id] + ], + 'crud-type' => 'delete' + ]; + + $response = $this->json('POST', self::$path . '/approve', $data, $this->headers); + + $salesReturn = SalesReturn::where('id', $salesReturn->id)->first(); + $salesReturnItem = $salesReturn->items[0]; + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + [ + 'id' => $salesReturn->id, + 'sales_invoice_id' => $salesReturn->sales_invoice_id, + 'warehouse_id' => $salesReturn->warehouse_id, + 'customer_id' => $salesReturn->customer_id, + 'customer_name' => $salesReturn->customer_name, + 'customer_address' => $salesReturn->customer_address, + 'customer_phone' => $salesReturn->customer_phone, + 'tax' => $salesReturn->tax, + 'amount' => $salesReturn->amount, + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.0.form.date'), + 'number' => $salesReturn->form->number, + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $salesReturn->form->notes, + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.0.form.updated_by'), + 'done' => $salesReturn->form->done, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $salesReturn->form->formable_id, + 'formable_type' => $salesReturn->form->formable_type, + 'request_approval_at' => $response->json('data.0.form.request_approval_at'), + 'request_approval_to' => $salesReturn->form->request_approval_to, + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.0.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, + 'approval_status' => $salesReturn->form->approval_status, + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.0.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.0.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => 1, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.0.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.0.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, + ] + ] + ] + ]); + + $subTotal = $response->json('data.0.amount') - $response->json('data.0.tax'); + $this->assertDatabaseHas('forms', [ + 'id' => $response->json('data.0.form.id'), + 'number' => $response->json('data.0.form.number'), + 'cancellation_status' => 1 + ], 'tenant'); + + $this->assertDatabaseHas('user_activities', [ + 'number' => $response->json('data.0.form.number'), + 'table_id' => $response->json('data.0.id'), + 'table_type' => 'SalesReturn', + 'activity' => 'Cancellation Approved by Email' + ], 'tenant'); + + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.0.form.id'), + 'chart_of_account_id' => $this->arCoa->id, + 'credit' => $response->json('data.0.amount').'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.0.form.id'), + 'chart_of_account_id' => $this->salesIncomeCoa->id, + 'debit' => $subTotal.'.000000000000000000000000000000' + ], 'tenant'); + $this->assertDatabaseMissing('journals', [ + 'form_id' => $response->json('data.0.form.id'), + 'chart_of_account_id' => $this->taxCoa->id, + 'debit' => $response->json('data.tax').'.000000000000000000000000000000' + ], 'tenant'); + } /** @test */ public function unauthorized_reject_by_email_sales_return() @@ -384,4 +506,95 @@ public function success_reject_sales_return() 'debit' => $response->json('data.0.tax').'.000000000000000000000000000000' ], 'tenant'); } + + /** @test */ + public function success_reject_delete_by_email_sales_return() + { + $this->delete_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $approver = $salesReturn->form->requestCancellationTo; + $approverToken = $this->findOrCreateToken($approver); + + $this->changeActingAs($approver, $salesReturn); + + $data = [ + 'action' => 'reject', + 'approver_id' => $salesReturn->form->request_cancellation_to, + 'token' => $approverToken->token, + 'resource-type' => 'SalesReturn', + 'ids' => [ + ['id' => $salesReturn->id] + ], + 'crud-type' => 'delete', + 'reason' => $this->faker->text(200) + ]; + + $response = $this->json('POST', self::$path . '/reject', $data, $this->headers); + $salesReturn = SalesReturn::where('id', $salesReturn->id)->first(); + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + [ + 'id' => $salesReturn->id, + 'sales_invoice_id' => $salesReturn->sales_invoice_id, + 'warehouse_id' => $salesReturn->warehouse_id, + 'customer_id' => $salesReturn->customer_id, + 'customer_name' => $salesReturn->customer_name, + 'customer_address' => $salesReturn->customer_address, + 'customer_phone' => $salesReturn->customer_phone, + 'tax' => $salesReturn->tax, + 'amount' => $salesReturn->amount, + 'form' => [ + 'id' => $salesReturn->form->id, + 'date' => $response->json('data.0.form.date'), + 'number' => $salesReturn->form->number, + 'edited_number' => $salesReturn->form->edited_number, + 'edited_notes' => $salesReturn->form->edited_notes, + 'notes' => $salesReturn->form->notes, + 'created_by' => $salesReturn->form->created_by, + 'updated_by' => $response->json('data.0.form.updated_by'), + 'done' => $salesReturn->form->done, + 'increment' => $salesReturn->form->increment, + 'increment_group' => $salesReturn->form->increment_group, + 'formable_id' => $salesReturn->form->formable_id, + 'formable_type' => $salesReturn->form->formable_type, + 'request_approval_at' => $response->json('data.0.form.request_approval_at'), + 'request_approval_to' => $salesReturn->form->request_approval_to, + 'approval_by' => $salesReturn->form->approval_by, + 'approval_at' => $response->json('data.0.form.approval_at'), + 'approval_reason' => $salesReturn->form->approval_reason, + 'approval_status' => $salesReturn->form->approval_status, + 'request_cancellation_to' => $salesReturn->form->request_cancellation_to, + 'request_cancellation_by' => $salesReturn->form->request_cancellation_by, + 'request_cancellation_at' => $response->json('data.0.form.request_cancellation_at'), + 'request_cancellation_reason' => $salesReturn->form->request_cancellation_reason, + 'cancellation_approval_at' => $response->json('data.0.form.cancellation_approval_at'), + 'cancellation_approval_by' => $salesReturn->form->cancellation_approval_by, + 'cancellation_approval_reason' => $salesReturn->form->cancellation_approval_reason, + 'cancellation_status' => -1, + 'request_close_to' => $salesReturn->form->request_close_to, + 'request_close_by' => $salesReturn->form->request_close_by, + 'request_close_at' => $response->json('data.0.form.request_close_at'), + 'request_close_reason' => $salesReturn->form->request_close_reason, + 'close_approval_at' => $response->json('data.0.form.close_approval_at'), + 'close_approval_by' => $salesReturn->form->close_approval_by, + 'close_status' => $salesReturn->form->close_status, + ] + ] + ] + ]); + $this->assertDatabaseHas('forms', [ + 'number' => $salesReturn->form->number, + 'cancellation_status' => -1, + 'done' => 0 + ], 'tenant'); + $this->assertDatabaseHas('user_activities', [ + 'number' => $salesReturn->form->number, + 'table_id' => $salesReturn->id, + 'table_type' => 'SalesReturn', + 'activity' => 'Cancellation Rejected by Email' + ], 'tenant'); + } } diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php index 875624797..8529c6d89 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php @@ -3,7 +3,7 @@ namespace Tests\Feature\Http\Sales\SalesReturn; use Tests\TestCase; - +use App\Model\Token; use App\Model\Form; use App\Model\Sales\SalesReturn\SalesReturn; use App\Helpers\Inventory\InventoryHelper; @@ -390,6 +390,8 @@ public function success_send_approval_sales_return() { $this->create_sales_return(); + $approverToken = Token::orderBy('id', 'asc')->delete(); + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data['ids'][] = ['id' => $salesReturn->id]; @@ -430,4 +432,139 @@ public function success_send_multiple_approval_sales_return() ] ]); } + + /** @test */ + public function success_read_approval_sales_return() + { + $this->create_sales_return(); + + $data = [ + 'join' => 'form,customer,items,item', + 'fields' => 'sales_return.*', + 'sort_by' => '-form.number', + 'group_by' => 'form.id', + 'filter_form'=>'notArchived;null', + 'filter_like'=>'{}', + 'filter_date_min'=>'{"form.date":"2022-05-01 00:00:00"}', + 'filter_date_max'=>'{"form.date":"2022-05-17 23:59:59"}', + 'includes'=>'form;customer;warehouse;items.item;items.allocation', + 'limit'=>10, + 'page' => 1 + ]; + + $response = $this->json('GET', self::$path . '/approval', $data, $this->headers); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'data' => [ + [ + 'id', + 'sales_invoice_id', + 'customer_id', + 'warehouse_id', + 'customer_name', + 'customer_address', + 'customer_phone', + 'tax', + 'amount', + 'form' => [ + 'id', + 'date', + 'number', + 'edited_number', + 'edited_notes', + 'notes', + 'created_by', + 'updated_by', + 'done', + 'increment', + 'increment_group', + 'formable_id', + 'formable_type', + 'request_approval_at', + 'request_approval_to', + 'approval_by', + 'approval_at', + 'approval_reason', + 'approval_status', + 'request_cancellation_to', + 'request_cancellation_by', + 'request_cancellation_at', + 'request_cancellation_reason', + 'cancellation_approval_at', + 'cancellation_approval_by', + 'cancellation_approval_reason', + 'cancellation_status', + 'request_close_to', + 'request_close_by', + 'request_close_at', + 'request_close_reason', + 'close_approval_at', + 'close_approval_by', + 'close_status' + ], + 'customer' => [ + 'id', + 'code', + 'tax_identification_number', + 'name', + 'address', + 'city', + 'state', + 'country', + 'zip_code', + 'latitude', + 'longitude', + 'phone', + 'phone_cc', + 'email', + 'notes', + 'credit_limit', + 'branch_id', + 'created_by', + 'updated_by', + 'archived_by', + 'pricing_group_id', + 'label' + ], + 'items' => [ + [ + 'id', + 'sales_return_id', + 'sales_invoice_item_id', + 'item_id', + 'item_name', + 'quantity', + 'quantity_sales', + 'price', + 'discount_percent', + 'discount_value', + 'unit', + 'converter', + 'expiry_date', + 'production_number', + 'notes', + 'allocation_id', + 'allocation', + ] + ] + ] + ], + 'links' => [ + 'first', + 'last', + 'prev', + 'next', + ], + 'meta' => [ + 'current_page', + 'from', + 'last_page', + 'path', + 'per_page', + 'to', + 'total', + ] + ]); + } } \ No newline at end of file diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php index 3098b9348..2ac93f298 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php @@ -297,6 +297,27 @@ public function error_reason_more_than_255_character_reject_cancel_sales_return( ] ]); } + + /** @test */ + public function error_already_rejected_reject_sales_return() + { + $this->delete_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + $salesReturn->form->cancellation_status = -1; + $salesReturn->form->save(); + + $data['reason'] = $this->faker->text(100); + + $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/cancellation-reject', $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form not in cancellation pending state' + ]); + } + /** @test */ public function error_empty_reason_reject_cancel_sales_return() diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php index 3a853f21a..0016d919d 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php @@ -111,30 +111,45 @@ public function read_sales_return_histories() $response = $this->json('GET', self::$path . '/' . $salesReturnUpdated->id . '/histories', $data, $this->headers); $response->assertStatus(200) - ->assertJson([ + ->assertJsonStructure([ 'data' => [ [ - 'id' => $response->json('data.0.id'), - 'table_type' => 'SalesReturn', - 'table_id' => $salesReturnUpdated->id, - 'number' => $salesReturnUpdated->form->number, - 'date' => $response->json('data.0.date'), - 'user_id' => $response->json('data.0.user_id'), - 'activity' => $response->json('data.0.activity'), - 'formable_id' => $salesReturnUpdated->id, + 'id', + 'table_type', + 'table_id', + 'number', + 'date', + 'user_id', + 'activity', + 'formable_id', 'user' => [ - 'id' => $response->json('data.0.user.id'), - 'name' => $response->json('data.0.user.name'), - 'first_name' => $response->json('data.0.user.first_name'), - 'last_name' => $response->json('data.0.user.last_name'), - 'address' => $response->json('data.0.user.address'), - 'phone' => $response->json('data.0.user.phone'), - 'email' => $response->json('data.0.user.email'), - 'branch_id' => $response->json('data.0.user.branch_id'), - 'warehouse_id' => $response->json('data.0.user.warehouse_id'), - 'full_name' => $response->json('data.0.user.full_name'), + 'id', + 'name', + 'first_name', + 'last_name', + 'address', + 'phone', + 'email', + 'branch_id', + 'warehouse_id', + 'full_name', ], ] + ], + 'links' => [ + 'first', + 'last', + 'prev', + 'next', + ], + 'meta' => [ + 'current_page', + 'from', + 'last_page', + 'path', + 'per_page', + 'to', + 'total', ] ]); diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php index eef859027..31a1248dd 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php @@ -11,11 +11,24 @@ use App\Model\Sales\SalesInvoice\SalesInvoice; use Illuminate\Support\Facades\Mail; use App\Helpers\Inventory\InventoryHelper; +use Throwable; class SalesReturnTest extends TestCase { use SalesReturnSetup; + public function create_sales_return($isFirstCreate = true) + { + $data = $this->getDummyData(); + + if($isFirstCreate) { + $this->setRole(); + $this->previousSalesReturnData = $data; + } + + $this->json('POST', self::$path, $data, $this->headers); + } + public static $path = '/api/v1/sales/return'; /** @test */ @@ -442,7 +455,7 @@ public function success_create_sales_return() /** @test */ public function unauthorized_no_branch_read_all_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $this->branchDefault->pivot->is_default = false; $this->branchDefault->pivot->save(); @@ -470,7 +483,7 @@ public function unauthorized_no_branch_read_all_sales_return() /** @test */ public function unauthorized_no_branch_read_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $this->branchDefault->pivot->is_default = false; $this->branchDefault->pivot->save(); @@ -495,7 +508,7 @@ public function unauthorized_no_branch_read_sales_return() /** @test */ public function unauthorized_read_all_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $this->unsetUserRole(); @@ -522,7 +535,7 @@ public function unauthorized_read_all_sales_return() /** @test */ public function unauthorized_read_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $this->unsetUserRole(); @@ -546,7 +559,7 @@ public function unauthorized_read_sales_return() /** @test */ public function success_read_all_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $data = [ 'join' => 'form,customer,items,item', @@ -698,7 +711,7 @@ public function success_read_all_sales_return() /** @test */ public function read_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $salesReturnItem = $salesReturn->items[0]; @@ -890,7 +903,7 @@ public function read_sales_return() /** @test */ public function unauthorized_no_default_branch_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $this->branchDefault->pivot->is_default = false; $this->branchDefault->pivot->save(); @@ -910,7 +923,7 @@ public function unauthorized_no_default_branch_update_sales_return() /** @test */ public function referenced_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -931,7 +944,7 @@ public function referenced_update_sales_return() /** @test */ public function unauthorized_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $this->unsetUserRole(); @@ -950,7 +963,7 @@ public function unauthorized_update_sales_return() /** @test */ public function invalid_data_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data = $this->getDummyData($salesReturn); @@ -969,7 +982,7 @@ public function invalid_data_update_sales_return() /** @test */ public function error_form_done_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $salesReturn->form->done = 1; @@ -990,7 +1003,7 @@ public function error_form_done_update_sales_return() /** @test */ public function error_notes_more_than_255_character_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1015,7 +1028,7 @@ public function error_notes_more_than_255_character_update_sales_return() /** @test */ public function whitespaces_trimmed_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1034,7 +1047,7 @@ public function whitespaces_trimmed_update_sales_return() /** @test */ public function overquantity_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1059,7 +1072,7 @@ public function overquantity_update_sales_return() /** @test */ public function invalid_total_item_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1079,7 +1092,7 @@ public function invalid_total_item_update_sales_return() /** @test */ public function invalid_sub_total_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1099,7 +1112,7 @@ public function invalid_sub_total_update_sales_return() /** @test */ public function invalid_tax_base_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1119,7 +1132,7 @@ public function invalid_tax_base_update_sales_return() /** @test */ public function invalid_type_of_tax_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1139,7 +1152,7 @@ public function invalid_type_of_tax_update_sales_return() /** @test */ public function invalid_tax_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1159,7 +1172,7 @@ public function invalid_tax_update_sales_return() /** @test */ public function invalid_amount_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1179,7 +1192,7 @@ public function invalid_amount_update_sales_return() /** @test */ public function error_journal_not_found_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1202,7 +1215,7 @@ public function error_journal_not_found_update_sales_return() /** @test */ public function check_journal_balance_update_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1215,11 +1228,35 @@ public function check_journal_balance_update_sales_return() $journal = SalesReturn::checkJournalBalance($salesReturn); $this->assertEquals($journal['debit'], $journal['credit']); } + + /** @test */ + public function will_throw_on_data_duplicate() + { + $this->expectException(Throwable::class); + $this->expectedExceptionMessageRegExp('\bIntegrity constraint violation\b'); + + $oldSalesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $response = $this->json('PATCH', self::$path . '/' . $oldSalesReturn->id, $data, $this->headers); + + $oldSalesReturn->form->number = $response->json('data.form.number'); + $oldSalesReturn->form->save(); + } + + public function approve_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); + + } /** @test */ public function success_update_sales_return() { - $this->success_create_sales_return(); + $this->approve_sales_return(); $oldSalesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1370,7 +1407,7 @@ public function success_update_sales_return() /** @test */ public function unauthorized_different_default_branch_delete_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $branch = $this->createBranch(); $this->branchDefault->pivot->branch_id = $branch->id; @@ -1392,7 +1429,7 @@ public function unauthorized_different_default_branch_delete_sales_return() /** @test */ public function unauthorized_no_warehouse_default_branch_delete_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $this->removeUserWarehouse(); @@ -1411,7 +1448,7 @@ public function unauthorized_no_warehouse_default_branch_delete_sales_return() /** @test */ public function unauthorized_delete_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $this->unsetUserRole(); @@ -1429,7 +1466,7 @@ public function unauthorized_delete_sales_return() /** @test */ public function error_form_done_delete_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $salesReturn->form->done = 1; @@ -1449,7 +1486,7 @@ public function error_form_done_delete_sales_return() /** @test */ public function referenced_delete_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $this->createPaymentCollection($salesReturn); @@ -1468,7 +1505,7 @@ public function referenced_delete_sales_return() /** @test */ public function error_empty_reason_delete_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -1489,7 +1526,7 @@ public function error_empty_reason_delete_sales_return() /** @test */ public function success_delete_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data['reason'] = $this->faker->text(200); From f4490c4e96b1ad79f2ff602433c38f45afc06076 Mon Sep 17 00:00:00 2001 From: Martien Dermawan Tanama Date: Wed, 21 Dec 2022 11:37:15 +0700 Subject: [PATCH 14/21] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..6981466b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 PointHub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From d9e20bfa4fa8e496b9f42d09e9c87ddb3f8f22cd Mon Sep 17 00:00:00 2001 From: Gunadi Wirawan Date: Thu, 29 Dec 2022 19:34:06 +0800 Subject: [PATCH 15/21] sales return: remove auto send email --- app/Model/Sales/SalesReturn/SalesReturn.php | 2 -- .../Sales/SalesReturn/SalesReturnHistoryTest.php | 4 ++-- .../Http/Sales/SalesReturn/SalesReturnTest.php | 12 ------------ 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/app/Model/Sales/SalesReturn/SalesReturn.php b/app/Model/Sales/SalesReturn/SalesReturn.php index 887524e95..d7a105e0f 100644 --- a/app/Model/Sales/SalesReturn/SalesReturn.php +++ b/app/Model/Sales/SalesReturn/SalesReturn.php @@ -78,8 +78,6 @@ public static function create($data) $form = new Form; $form->saveData($data, $salesReturn); - self::sendApproval($salesReturn); - return $salesReturn; } diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php index 3a853f21a..4b02602e8 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php @@ -116,12 +116,12 @@ public function read_sales_return_histories() [ 'id' => $response->json('data.0.id'), 'table_type' => 'SalesReturn', - 'table_id' => $salesReturnUpdated->id, + 'table_id' => $response->json('data.0.table_id'), 'number' => $salesReturnUpdated->form->number, 'date' => $response->json('data.0.date'), 'user_id' => $response->json('data.0.user_id'), 'activity' => $response->json('data.0.activity'), - 'formable_id' => $salesReturnUpdated->id, + 'formable_id' => $response->json('data.0.formable_id'), 'user' => [ 'id' => $response->json('data.0.user.id'), 'name' => $response->json('data.0.user.name'), diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php index eef859027..7022f4a83 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php @@ -311,12 +311,8 @@ public function success_create_sales_return() $data = $this->getDummyData(); - Mail::fake(); - $response = $this->json('POST', self::$path, $data, $this->headers); - Mail::assertQueued(SalesReturnApprovalRequest::class); - $salesReturn = SalesReturn::where('id', $response->json('data.id'))->first(); $this->assertIsObject( @@ -1226,12 +1222,8 @@ public function success_update_sales_return() $data = $this->getDummyData($oldSalesReturn); $data = data_set($data, 'id', $oldSalesReturn->id, false); - Mail::fake(); - $response = $this->json('PATCH', self::$path . '/' . $oldSalesReturn->id, $data, $this->headers); - Mail::assertQueued(SalesReturnApprovalRequest::class); - $salesReturn = SalesReturn::where('id', $response->json('data.id'))->first(); $this->assertIsObject( @@ -1494,14 +1486,10 @@ public function success_delete_sales_return() $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); $data['reason'] = $this->faker->text(200); - Mail::fake(); - $response = $this->json('DELETE', self::$path . '/' . $salesReturn->id, $data, $this->headers); $response->assertStatus(204); - Mail::assertQueued(SalesReturnApprovalRequest::class); - $this->assertDatabaseHas('forms', [ 'number' => $salesReturn->form->number, 'request_cancellation_reason' => $data['reason'], From a41458724a3de7438b731e7d14acaf0618e23a73 Mon Sep 17 00:00:00 2001 From: "@gunadi" Date: Mon, 30 Jan 2023 11:05:30 +0800 Subject: [PATCH 16/21] sales return: fix email from --- app/Mail/Sales/SalesReturnApprovalRequest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Mail/Sales/SalesReturnApprovalRequest.php b/app/Mail/Sales/SalesReturnApprovalRequest.php index 8c56fa8a0..8e90f36c4 100644 --- a/app/Mail/Sales/SalesReturnApprovalRequest.php +++ b/app/Mail/Sales/SalesReturnApprovalRequest.php @@ -42,7 +42,6 @@ public function build() $user = $this->form->send_by; if (count($this->salesReturns) > 1) { return $this->subject('Request Approval All') - ->from($user->email, $user->getFullNameAttribute()) ->view('emails.sales.return.return-approval-request', [ 'salesReturns' => $this->salesReturns, 'approver' => $this->approver, @@ -56,7 +55,6 @@ public function build() } return $this->subject('Approval Request') - ->from($user->email, $user->getFullNameAttribute()) ->view('emails.sales.return.return-approval-request-single', [ 'salesReturns' => $this->salesReturns, 'approver' => $this->approver, From aae6932732622c1728c51058eaf35bbdd754c413 Mon Sep 17 00:00:00 2001 From: "@gunadi" Date: Tue, 31 Jan 2023 10:54:20 +0800 Subject: [PATCH 17/21] sales return: update request approval query --- .../Api/Sales/SalesReturn/SalesReturnApprovalController.php | 6 ------ .../Api/Sales/SalesReturn/SalesReturnController.php | 1 - 2 files changed, 7 deletions(-) diff --git a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalController.php b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalController.php index c7cf13b18..adb03c9a2 100644 --- a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalController.php +++ b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalController.php @@ -31,12 +31,6 @@ public function index(Request $request) $salesReturns = SalesReturn::joins($salesReturns, $request->get('join')) ->whereNull(Form::$alias . '.edited_number') ->where(Form::$alias . '.close_status', 0) - ->orWhere(function ($query) { - $query - ->where(Form::$alias . '.cancellation_status', 0) - ->whereNull(Form::$alias . '.close_status') - ->whereNull(Form::$alias . '.edited_number'); - }) ->orWhere(function ($query) { $query ->where(Form::$alias . '.approval_status', 0) diff --git a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnController.php b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnController.php index c27fd18dd..1eec023f5 100644 --- a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnController.php +++ b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnController.php @@ -125,7 +125,6 @@ public function destroy(Request $request, $id) $request->validate([ 'reason' => 'required']); $salesReturn->requestCancel($request); - SalesReturn::sendApproval($salesReturn); return response()->json([], 204); From f165e26ebac6506db3f35f55043b7555ffa52750 Mon Sep 17 00:00:00 2001 From: Gunadi Wirawan Date: Sat, 4 Feb 2023 18:24:03 +0800 Subject: [PATCH 18/21] sales return: get mail url from referer --- app/Mail/Sales/SalesReturnApprovalRequest.php | 17 +++++++++-------- .../return/return-approval-request.blade.php | 10 +++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/Mail/Sales/SalesReturnApprovalRequest.php b/app/Mail/Sales/SalesReturnApprovalRequest.php index 8c56fa8a0..b5bff44eb 100644 --- a/app/Mail/Sales/SalesReturnApprovalRequest.php +++ b/app/Mail/Sales/SalesReturnApprovalRequest.php @@ -39,6 +39,12 @@ public function build() { $this->approver->token = $this->approverToken; + if (@$this->urlReferer) { + $parsedUrl = parse_url($this->urlReferer); + $port = @$parsedUrl['port'] ? ":{$parsedUrl['port']}" : ''; + $url = "{$parsedUrl['scheme']}://{$parsedUrl['host']}{$port}/"; + } + $user = $this->form->send_by; if (count($this->salesReturns) > 1) { return $this->subject('Request Approval All') @@ -46,22 +52,17 @@ public function build() ->view('emails.sales.return.return-approval-request', [ 'salesReturns' => $this->salesReturns, 'approver' => $this->approver, - 'form' => $this->form + 'form' => $this->form, + 'url' => @$url ]); } else { - if (@$this->urlReferer) { - $parsedUrl = parse_url($this->urlReferer); - $port = @$parsedUrl['port'] ? ":{$parsedUrl['port']}" : ''; - $url = "{$parsedUrl['scheme']}://{$parsedUrl['host']}{$port}/"; - } - return $this->subject('Approval Request') ->from($user->email, $user->getFullNameAttribute()) ->view('emails.sales.return.return-approval-request-single', [ 'salesReturns' => $this->salesReturns, 'approver' => $this->approver, 'form' => $this->form, - 'url' => @$url, + 'url' => @$url ]); } } diff --git a/resources/views/emails/sales/return/return-approval-request.blade.php b/resources/views/emails/sales/return/return-approval-request.blade.php index 5e427af0f..19bf344c5 100644 --- a/resources/views/emails/sales/return/return-approval-request.blade.php +++ b/resources/views/emails/sales/return/return-approval-request.blade.php @@ -108,19 +108,19 @@
Check Approve Reject @@ -157,13 +157,13 @@ $urlApprovalQueries['ids'] = implode(",", Illuminate\Support\Arr::pluck($salesReturns, 'id')); @endphp Approve All Reject All From aab8ba144cd7df9ee4aa606e947eee796a22c674 Mon Sep 17 00:00:00 2001 From: Gunadi Wirawan Date: Sat, 4 Feb 2023 18:24:37 +0800 Subject: [PATCH 19/21] sales return: get mail url from referer --- .../sales/return/return-approval-request.blade.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/views/emails/sales/return/return-approval-request.blade.php b/resources/views/emails/sales/return/return-approval-request.blade.php index 19bf344c5..23adc904e 100644 --- a/resources/views/emails/sales/return/return-approval-request.blade.php +++ b/resources/views/emails/sales/return/return-approval-request.blade.php @@ -108,19 +108,19 @@
Check Approve Reject @@ -157,13 +157,13 @@ $urlApprovalQueries['ids'] = implode(",", Illuminate\Support\Arr::pluck($salesReturns, 'id')); @endphp Approve All Reject All From 493ef078364bcbf0518288bf79b5ba2d47005b1a Mon Sep 17 00:00:00 2001 From: Martien Dermawan Tanama Date: Thu, 11 May 2023 10:12:27 +0700 Subject: [PATCH 20/21] fix --- .../Inventory/InventoryUsage/InventoryUsage.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/app/Model/Inventory/InventoryUsage/InventoryUsage.php b/app/Model/Inventory/InventoryUsage/InventoryUsage.php index a98fa811e..3ab78d72a 100644 --- a/app/Model/Inventory/InventoryUsage/InventoryUsage.php +++ b/app/Model/Inventory/InventoryUsage/InventoryUsage.php @@ -150,17 +150,6 @@ public static function updateJournal($usage) self::checkIsJournalBalance($usage); } - private static function checkIsItemQuantityOver($item, $itemModel, $inventoryUsage, $options = [ - 'expiry_date' => null, - 'production_number' => null, - ]) - { - $stock = InventoryHelper::getCurrentStock($itemModel, $inventoryUsage->created_at, $inventoryUsage->warehouse, $options); - if (abs($item['quantity']) > $stock) { - throw new StockNotEnoughException($itemModel); - } - } - private static function mapItems($items, $inventoryUsage) { $array = []; @@ -179,7 +168,6 @@ private static function mapItems($items, $inventoryUsage) 'expiry_date' => $dna['expiry_date'], 'production_number' => $dna['production_number'], ]; - self::checkIsItemQuantityOver($item, $itemModel, $inventoryUsage, $options); $dnaItem = $item; $dnaItem['quantity'] = $dna['quantity']; @@ -190,8 +178,6 @@ private static function mapItems($items, $inventoryUsage) } } } else { - self::checkIsItemQuantityOver($item, $itemModel, $inventoryUsage); - array_push($array, $item); } } From e44c065d36b5f2c598423078fff4a0f9c6c436c6 Mon Sep 17 00:00:00 2001 From: Martien Dermawan Tanama Date: Wed, 24 May 2023 13:29:47 +0700 Subject: [PATCH 21/21] remove limit --- app/helpers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers.php b/app/helpers.php index 3c6913b2a..62fcc0019 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -77,7 +77,7 @@ function pagination($query, $limit = null) } // limit call maximum 1000 item per page - $limit = $limit > 1000 ? 1000 : $limit; + $limit = $limit > 1000000 ? 1000000 : $limit; return $query->paginate($limit); }