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. 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/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..464ec5846 100644 --- a/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php +++ b/app/Http/Controllers/Api/Sales/SalesReturn/SalesReturnApprovalByEmailController.php @@ -11,6 +11,10 @@ use App\Model\UserActivity; 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 { @@ -37,12 +41,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 && $salesReturn->form->cancellation_status === null) { + 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 +91,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 +112,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..adb03c9a2 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 @@ -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) @@ -68,27 +62,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 +105,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 +117,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..1eec023f5 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); @@ -127,5 +127,7 @@ public function destroy(Request $request, $id) $salesReturn->requestCancel($request); return response()->json([], 204); + + } } 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(); 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/Mail/Sales/SalesReturnApprovalRequest.php b/app/Mail/Sales/SalesReturnApprovalRequest.php index 8c56fa8a0..732872607 100644 --- a/app/Mail/Sales/SalesReturnApprovalRequest.php +++ b/app/Mail/Sales/SalesReturnApprovalRequest.php @@ -39,29 +39,28 @@ 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') - ->from($user->email, $user->getFullNameAttribute()) ->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/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); } } 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..d7a105e0f 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,6 +71,8 @@ public static function create($data) $salesReturn->save(); $salesReturn->items()->saveMany($items); + + self::checkJournalBalance($salesReturn); //$salesReturn->services()->saveMany($services); $form = new Form; @@ -73,6 +81,52 @@ public static function create($data) 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 +208,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 +276,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/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; + } +} 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); } 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 } } 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/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: 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..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 @@ -138,13 +138,13 @@ Check Approve Reject 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..23adc904e 100644 --- a/resources/views/emails/sales/return/return-approval-request.blade.php +++ b/resources/views/emails/sales/return/return-approval-request.blade.php @@ -48,15 +48,8 @@ Form Number Form Reference Customer - - - - - - - -
ItemQuantity Return
- + Item + Quantity Return Note Created By Created At @@ -72,13 +65,13 @@ $urlApprovalQueries['crud-type'] = $salesReturn->action; @endphp - + {{ $loop->iteration }} - + {{ date('d M Y', strtotime($salesReturnForm->date)) }} - + {{ $salesReturnForm->number }} {{ ' ' }} {{ @@ -88,55 +81,46 @@ : '' }} - + {{ $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 }} -
+ @foreach($salesReturn->items as $item) + + {{ $item->item->name }} - - {{ $item->note }} + + {{ $item->quantity }} - + @break + @endforeach + + {{ $salesReturnForm->notes }} + + {{ $salesReturnForm->createdBy->getFullNameAttribute() }} - + {{ date('d M Y, H:i', strtotime($salesReturnForm->created_at)) }} - -
+ +
Check Approve Reject @@ -144,6 +128,25 @@
+ @php + ($first = true); + @endphp + @foreach($salesReturn->items as $item) + @if($first) + @php + ($first = false); + @endphp + @continue + @endif + + + {{ $item->item->name }} + + + {{ $item->quantity }} + + + @endforeach @endforeach @@ -154,13 +157,13 @@ $urlApprovalQueries['ids'] = implode(",", Illuminate\Support\Arr::pluck($salesReturns, 'id')); @endphp Approve All Reject All diff --git a/routes/api/purchase.php b/routes/api/purchase.php index 800ab4b31..8c487f630 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..2a542e00c --- /dev/null +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestApprovalTest.php @@ -0,0 +1,209 @@ +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(); + + $data = [ + 'id' => $this->purchase->id, + 'reason' => 'reason' + ]; + + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/reject', $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + "code" => 422, + "message" => "Unauthorized" + ]); + } + + /** @test */ + 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); + $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 success_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)->assertJson([ + "code" => 422, + "message" => "Not Authorized" + ]); + /* e: bulk approval email fail test */ + } + + /** @test */ + public function success_reject_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 */ + } + + /** @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 */ + } + + /** @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 diff --git a/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php b/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php new file mode 100644 index 000000000..de00cd1ce --- /dev/null +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestCloseTest.php @@ -0,0 +1,159 @@ +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_data_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(422)->assertJson([ + "code" => 422, + "message" => "The given data was invalid." + ]); + } + + /** @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 or 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); + + $this->assertDatabaseHas('forms', [ + 'number' => $this->purchase->form->number, + 'close_status' => true + ], 'tenant'); + } + + // /** @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() + { + // $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/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 new file mode 100644 index 000000000..282b9b3fb --- /dev/null +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestSetup.php @@ -0,0 +1,209 @@ +setupUser(); + $this->setProject(); + $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() + { + 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 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; + $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; + } + + 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) + ->where('model_type', 'App\Model\Master\User') + ->where('model_id', $this->user->id) + ->delete(); + } + + protected function setDefaultBranch($state = true) + { + foreach ($this->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..203aaab95 --- /dev/null +++ b/tests/Feature/Http/Purchase/Request/PurchaseRequestTest.php @@ -0,0 +1,483 @@ + 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->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 */ + 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' => $this->convertDateTime($this->purchase->required_date) + ], '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(); + + $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_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(); + + // 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); + } + + /** @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)->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 */ + 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 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() + { + //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)->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 */ + 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, + '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)->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 */ + public function success_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' => $this->userPassword, + 'reason' => 'Reason' + ]; + $response = $this->json('DELETE', self::$path.'/'.$this->purchase->id, $data, $this->headers); + $response->assertStatus(204); + + $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 */ + public function success_approve_delete_purchase_request() + { + $this->success_delete_purchase_request(); + + $data = [ + 'id' => $this->purchase->id + ]; + + $response = $this->json('POST', self::$path.'/'.$this->purchase->id.'/cancellation-approve', $data, $this->headers); + $response->assertStatus(200); + } + + /** @test */ + public function failed_reject_delete_purchase_request() + { + $this->success_delete_purchase_request(); + + $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); + } +} \ No newline at end of file diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalByEmailTest.php index f9dee68fe..89c3c9057 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,68 +48,82 @@ 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); - $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); + $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'); + 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_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 success_approve_sales_return() + public function error_already_approved_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(); - $response = $this->json('POST', self::$path . '/' . $salesReturn->id . '/approve', [], $this->headers); - $response->assertStatus(200); - $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'); + $approver = $salesReturn->form->requestApprovalTo; + $approverToken = $this->findOrCreateToken($approver); + + $this->changeActingAs($approver, $salesReturn); + + $data = [ + 'action' => 'approve', + 'approver_id' => $salesReturn->form->request_approval_to, + 'token' => $approverToken->token, + 'resource-type' => 'SalesReturn', + 'ids' => [ + ['id' => $salesReturn->id] + ], + 'crud-type' => 'delete' + ]; + + $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 unauthorized_approve_by_email_sales_return() { - $this->success_delete_sales_return(); + $this->create_sales_return(); $this->unsetUserRole(); @@ -119,11 +135,11 @@ public function unauthorized_approve_by_email_sales_return() "message" => "There is no permission named `approve sales return` for guard `api`." ]); } - - /** @test */ + + /** @test */ public function success_approve_by_email_sales_return() { - $this->success_create_sales_return(); + $this->create_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -143,33 +159,122 @@ public function success_approve_by_email_sales_return() 'crud-type' => 'delete' ]; + $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); - - $response->assertStatus(200); + $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' => 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', [ - '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 ], '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' => '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('inventories', [ - 'form_id' => $salesReturn->form->id, - 'item_id' => $salesReturn->items()->first()->item_id, - 'quantity' => $salesReturn->items()->first()->quantity, + $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 */ public function success_approve_delete_by_email_sales_return() { - $this->success_delete_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); @@ -189,23 +294,96 @@ public function success_approve_delete_by_email_sales_return() $response = $this->json('POST', self::$path . '/approve', $data, $this->headers); - $response->assertStatus(200); + $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', [ - 'number' => $salesReturn->form->number, - 'cancellation_status' => 1, + 'id' => $response->json('data.0.form.id'), + 'number' => $response->json('data.0.form.number'), + 'cancellation_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' ], 'tenant'); - } - /** @test */ + $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() { - $this->success_delete_sales_return(); + $this->create_sales_return(); $this->unsetUserRole(); @@ -219,9 +397,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(); @@ -238,30 +416,101 @@ 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, + '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, + ] + ] + ] + ]); - $response->assertStatus(200); $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'); + + $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->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'); } /** @test */ public function success_reject_delete_by_email_sales_return() { - $this->success_delete_sales_return(); + $this->delete_sales_return(); $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); @@ -278,12 +527,64 @@ public function success_reject_delete_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); - - $response->assertStatus(200); + $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, diff --git a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php index 510479b41..8529c6d89 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnApprovalTest.php @@ -3,210 +3,568 @@ 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; 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); - $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); - $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); - } - - /** @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); - $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); - } - - /** @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); - } - - /** @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); - } + '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(); + + $approverToken = Token::orderBy('id', 'asc')->delete(); + + $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'] + ] + ]); + } + + /** @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 6b6eaa9f4..2ac93f298 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnCancellationApprovalTest.php @@ -5,166 +5,356 @@ 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); - $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); + $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); - $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); - } - - /** @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); - } - - /** @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); - $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_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() + { + $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 6f426e2c1..0761f6fcf 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnHistoryTest.php @@ -13,48 +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); - $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); - $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(); @@ -70,7 +84,76 @@ public function read_sales_return_histories() $response = $this->json('GET', self::$path . '/' . $salesReturnUpdated->id . '/histories', $data, $this->headers); - $response->assertStatus(200); + $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' => [ + [ + 'id' => $response->json('data.0.id'), + 'table_type' => 'SalesReturn', + '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' => $response->json('data.0.formable_id'), + 'user' => [ + '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', + ] + ]); + + $this->assertGreaterThan(0, count($response->json('data'))); $this->assertDatabaseHas('user_activities', [ 'number' => $salesReturn->form->edited_number, 'table_id' => $salesReturn->id, @@ -84,10 +167,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 = [ @@ -97,7 +181,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..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; @@ -51,6 +52,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) @@ -64,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() { @@ -130,9 +140,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 +169,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 +198,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 +227,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; @@ -261,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, @@ -272,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, ], @@ -317,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, @@ -329,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, @@ -375,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 b2598a248..65ec49308 100644 --- a/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php +++ b/tests/Feature/Http/Sales/SalesReturn/SalesReturnTest.php @@ -4,288 +4,1533 @@ 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; +use Throwable; class SalesReturnTest extends TestCase { use SalesReturnSetup; - public static $path = '/api/v1/sales/return'; - - /** @test */ - public function unauthorized_create_sales_return() + public function create_sales_return($isFirstCreate = true) { $data = $this->getDummyData(); + + if($isFirstCreate) { + $this->setRole(); + $this->previousSalesReturnData = $data; + } - $response = $this->json('POST', self::$path, $data, $this->headers); + $this->json('POST', self::$path, $data, $this->headers); + } + + public static $path = '/api/v1/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); - - $response->assertStatus(422); - } - /** @test */ - public function success_create_sales_return() - { - $this->setRole(); - - $data = $this->getDummyData(); - - $response = $this->json('POST', self::$path, $data, $this->headers); - - $response->assertStatus(201); - $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_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); + + $journal = SalesReturn::checkJournalBalance($salesReturn); + $this->assertEquals($journal['debit'], $journal['credit']); + } + + /** @test */ + public function success_create_sales_return() + { + $this->setRole(); + + $data = $this->getDummyData(); + + $response = $this->json('POST', self::$path, $data, $this->headers); + + $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' => $response->json('data.form.number'), - 'approval_by' => $response->json('data.form.approval_by'), - 'approval_status' => 1, + '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('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() - { - $this->success_create_sales_return(); - + + $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->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;null', - 'filter_like' => '{}', - 'limit' => 10, - 'includes' => 'form;customer;items.item;items.allocation', - 'page' => 1 + '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); - $this->assertGreaterThan(0, count($response->json('data'))); - } - - /** @test */ - public function read_sales_return() - { - $this->success_approve_sales_return(); - + $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->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->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->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->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->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;warehouse;items.item;items.allocation;salesInvoice.form;form.createdBy;form.requestApprovalTo;form.branch' + '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); - } - - /** @test */ - public function unauthorized_update_sales_return() - { - $this->success_create_sales_return(); - + + $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->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->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->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 referenced_update_sales_return() - { - $this->success_create_sales_return(); - + ->assertJson([ + 'code' => 0, + 'message' => 'There is no permission named `update sales return` for guard `api`.' + ]); + } + + /** @test */ + public function invalid_data_update_sales_return() + { + $this->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, '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->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) - ->assertJsonFragment(['message' => 'Cannot edit form because referenced by payment collection']); - } - - /** @test */ - public function overquantity_update_sales_return() - { - $this->success_create_sales_return(); - + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form already done' + ]); + } + + /** @test */ + public function error_notes_more_than_255_character_update_sales_return() + { + $this->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->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->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" => "Sales return item can't exceed sales invoice qty" - ]); - } - - /** @test */ - public function invalid_update_sales_return() - { - $this->success_create_sales_return(); - + ->assertJson([ + 'code' => 422, + 'message' => 'Sales return item can\'t exceed sales invoice qty' + ]); + } + + /** @test */ + public function invalid_total_item_update_sales_return() + { + $this->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); - + $data = data_set($data, 'items.0.total', 20000); + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); - - $response->assertStatus(422); - } - - /** @test */ - public function success_update_sales_return() - { - $this->success_create_sales_return(); - + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'total for item ' .$data['items'][0]['item_name']. ' should be 30000' + ]); + } + + /** @test */ + public function invalid_sub_total_update_sales_return() + { + $this->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, 'sub_total', 20000); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'sub total should be 30000' + ]); + } + + /** @test */ + public function invalid_tax_base_update_sales_return() + { + $this->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, 'tax_base', 20000); + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'tax base should be 30000' + ]); + } + + /** @test */ + public function invalid_type_of_tax_update_sales_return() + { + $this->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, 'type_of_tax', 'exclude'); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'type of tax should be same with invoice' + ]); + } + + /** @test */ + public function invalid_tax_update_sales_return() + { + $this->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, 'tax', 3000); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'tax should be 2727.2727272727' + ]); + } + + /** @test */ + public function invalid_amount_update_sales_return() + { + $this->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, 'amount', 40000); + + $response = $this->json('PATCH', self::$path . '/' . $salesReturn->id, $data, $this->headers); + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'amount should be 30000' + ]); + } + + /** @test */ + public function error_journal_not_found_update_sales_return() + { + $this->create_sales_return(); + + $salesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($salesReturn); + $data = data_set($data, 'id', $salesReturn->id, false); + + $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' => 'Journal sales account - account receivable not found' + ]); + } + + /** @test */ + public function check_journal_balance_update_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); + + $salesReturn = SalesReturn::where('id', $response->json('data.id'))->first(); + $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); - $response->assertStatus(201); - $this->assertDatabaseHas('forms', [ 'edited_number' => $response->json('data.form.number') ], 'tenant'); + } + + /** @test */ + public function success_update_sales_return() + { + $this->approve_sales_return(); + + $oldSalesReturn = SalesReturn::orderBy('id', 'asc')->first(); + + $data = $this->getDummyData($oldSalesReturn); + $data = data_set($data, 'id', $oldSalesReturn->id, false); + + $response = $this->json('PATCH', self::$path . '/' . $oldSalesReturn->id, $data, $this->headers); + + $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' => $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'); - } - - /** @test */ - public function unauthorized_delete_sales_return() - { - $this->success_create_sales_return(); - + + $this->assertDatabaseHas('forms', [ + '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'), + '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_different_default_branch_delete_sales_return() + { + $this->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->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->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() - { - $this->success_create_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->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->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() - { - $this->success_create_sales_return(); - + + $response->assertStatus(422) + ->assertJson([ + 'code' => 422, + 'message' => 'form referenced by payment collection' + ]); + } + + /** @test */ + public function error_empty_reason_delete_sales_return() + { + $this->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->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, + 'number' => $salesReturn->form->number, + 'request_cancellation_reason' => $data['reason'], + 'cancellation_status' => 0, ], 'tenant'); - } + } } \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 3abc80670..c2e358847 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; @@ -38,8 +39,11 @@ abstract class TestCase extends BaseTestCase * @var null|User */ protected $user; + protected $userPassword; protected $account = null; + protected $employee = null; + protected $role = null; /** * Set up the test. @@ -73,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'); @@ -143,6 +150,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 +167,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()