diff --git a/app/Http/Controllers/Api/Finance/Payment/PaymentCancellationApprovalController.php b/app/Http/Controllers/Api/Finance/Payment/PaymentCancellationApprovalController.php
new file mode 100644
index 000000000..02167d11d
--- /dev/null
+++ b/app/Http/Controllers/Api/Finance/Payment/PaymentCancellationApprovalController.php
@@ -0,0 +1,183 @@
+isCancellationRequestStillValid($payment);
+ if ($request->has('token')) {
+ // approve from email
+ $approvalBy = $request->get('approver_id');
+ } else {
+ // Jika Role Bukan Super Admin dan tidak memiliki akses approval maka mengirimkan pesan eror
+ $payment->isHaveAccessToDelete();
+ $approvalBy = auth()->user()->id;
+ }
+
+ DB::connection('tenant')->transaction(function () use ($payment, $approvalBy) {
+ // ### If approve success then
+ // Status form cash out akan menjadi cancelled
+ $payment->form->cancellation_approval_by = $approvalBy;
+ $payment->form->cancellation_approval_at = now();
+ $payment->form->cancellation_status = 1;
+ $payment->form->save();
+
+ // Jumlah saldo cash account / cash advance/ biaya yang dipilih akan bertambah sebesar data yang dihapus
+ // Pengembalian dana cash advance & status formnya menjadi pending
+ $amountPaidByCashAdvance = 0;
+ if ($payment->cashAdvance) {
+ $cashAdvancePayment = $payment->cashAdvance;
+ $cashAdvance = $cashAdvancePayment->cashAdvance;
+ $amountPaidByCashAdvance = $payment->cashAdvance->amount;
+
+ $cashAdvance->amount_remaining += $amountPaidByCashAdvance;
+ $cashAdvance->save();
+
+ $cashAdvance->form->done = 0;
+ $cashAdvance->form->save();
+
+ $activity = 'Payment Refund (' . $payment->form->number . ')';
+ $this->writeHistory($cashAdvance, $approvalBy, $activity);
+ }
+
+ // Pengembalian dana account
+ $amountPaidByAccount = $payment->amount - $amountPaidByCashAdvance;
+ $journal = new Journal;
+ $journal->form_id = $payment->form->id;
+ $journal->journalable_type = $payment->paymentable_type;
+ $journal->journalable_id = $payment->paymentable_id;
+ $journal->chart_of_account_id = $payment->payment_account_id;
+ if ($payment->disbursed) {
+ $journal->debit = $amountPaidByAccount;
+ } else {
+ $journal->credit = $amountPaidByAccount;
+ }
+ $journal->save();
+
+ foreach ($payment->details as $paymentDetail) {
+ $journal = new Journal;
+ $journal->form_id = $payment->form->id;
+ $journal->form_id_reference = optional(optional($paymentDetail->referenceable)->form)->id;
+ $journal->journalable_type = $payment->paymentable_type;
+ $journal->journalable_id = $payment->paymentable_id;
+ $journal->notes = $paymentDetail->notes;
+ $journal->chart_of_account_id = $paymentDetail->chart_of_account_id;
+ if (!$payment->disbursed) {
+ $journal->credit = $paymentDetail->amount;
+ } else {
+ $journal->debit = $paymentDetail->amount;
+ }
+ $journal->save();
+
+ // Status form reference jadi pending
+ $paymentDetail->referenceable->form->done = 0;
+ $paymentDetail->referenceable->form->save();
+ }
+
+ // Delete data allocation pada allocation report
+ $payment->allocationReports()->delete();
+ });
+
+ $payment->load('form');
+ return new ApiResource($payment);
+ }
+
+ /**
+ * @param Request $request
+ * @param $id
+ * @return ApiResource
+ * @throws ApprovalNotFoundException
+ * @throws UnauthorizedException
+ */
+ public function reject(Request $request, $id)
+ {
+ $payment = Payment::findOrFail($id);
+
+ // ### Reject fail if
+ $this->isCancellationRequestStillValid($payment);
+ if ($request->has('token')) {
+ // reject from email
+ $approvalBy = $request->get('approver_id');
+ $request->merge(['reason' => 'Rejected by email']);
+ } else {
+ $request->validate([
+ 'reason' => 'required'
+ ]);
+ // Jika Role Bukan Super Admin / Pihak yang dipilih utk approval maka akan mengirimkan pesan eror
+ // Jika tidak memiliki akses approval pada payment order maka akan mengirimkan pesan eror
+ $payment->isHaveAccessToDelete();
+ $approvalBy = auth()->user()->id;
+ }
+
+ DB::connection('tenant')->transaction(function () use ($payment, $request, $approvalBy) {
+ // ### If reject success then
+ // Update status approval form menjadi rejected
+ $payment->form->approval_status = -1;
+
+ // Update status form status menjadi pending
+ $payment->form->done = 0;
+
+ $payment->form->cancellation_approval_by = $approvalBy;
+ $payment->form->cancellation_approval_at = now();
+ $payment->form->cancellation_approval_reason = $request->get('reason');
+ $payment->form->cancellation_status = -1;
+
+ $payment->form->save();
+ });
+
+ $payment->form = $payment->form;
+ return new ApiResource($payment);
+ }
+
+ public function isCancellationRequestStillValid($payment)
+ {
+ // is cancellation request already approved?
+ $cancellationStatus = $payment->form->cancellation_status;
+ if ($cancellationStatus == 1) {
+ throw new PointException('Form cancellation already approved');
+ }
+ // is cancellation request already rejected?
+ // if ($cancellationStatus == -1) {
+ // throw new PointException('Form cancellation already rejected');
+ // }
+ }
+
+ // $reference = cash advance,
+ public function writeHistory($reference, int $userId, string $activity)
+ {
+ $history = new UserActivity;
+
+ $history->table_type = $reference::$morphName;
+ $history->table_id = $reference->id;
+ $history->number = $reference->form->number;
+ $history->user_id = $userId;
+ $history->date = convert_to_local_timezone(date('Y-m-d H:i:s'));
+ $history->activity = $activity;
+
+ $history->save();
+ }
+}
diff --git a/app/Http/Controllers/Api/Finance/Payment/PaymentController.php b/app/Http/Controllers/Api/Finance/Payment/PaymentController.php
index 94d47160b..fc46cac4a 100644
--- a/app/Http/Controllers/Api/Finance/Payment/PaymentController.php
+++ b/app/Http/Controllers/Api/Finance/Payment/PaymentController.php
@@ -7,10 +7,19 @@
use App\Http\Requests\Finance\Payment\Payment\UpdatePaymentRequest;
use App\Http\Resources\ApiCollection;
use App\Http\Resources\ApiResource;
+use App\Mail\Finance\Payment\PaymentCancellationApprovalRequest;
+use App\Model\Auth\Role;
use App\Model\Finance\Payment\Payment;
+use App\Model\Finance\PaymentOrder\PaymentOrder;
+use App\Model\Master\User;
+use App\Model\Purchase\PurchaseDownPayment\PurchaseDownPayment;
+use App\Model\Token;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Mail;
use Throwable;
class PaymentController extends Controller
@@ -23,7 +32,7 @@ class PaymentController extends Controller
*/
public function index(Request $request)
{
- $payment = Payment::from(Payment::getTableName().' as '.Payment::$alias)->eloquentFilter($request);
+ $payment = Payment::from(Payment::getTableName() . ' as ' . Payment::$alias)->eloquentFilter($request);
$payment = Payment::joins($payment, $request->get('join'));
@@ -47,7 +56,8 @@ public function store(StorePaymentRequest $request)
->load('form')
->load('paymentable')
->load('details.allocation')
- ->load('details.referenceable.form');
+ ->load('details.referenceable.form')
+ ->load('cashAdvance.cashAdvance.form');
return new ApiResource($payment);
});
@@ -83,7 +93,7 @@ public function update(UpdatePaymentRequest $request, $id)
$payment->form->archive();
foreach ($payment->details as $paymentDetail) {
- if (! $paymentDetail->isDownPayment()) {
+ if (!$paymentDetail->isDownPayment()) {
$reference = $paymentDetail->referenceable;
$reference->remaining += $paymentDetail->amount;
$reference->save();
@@ -114,23 +124,123 @@ public function update(UpdatePaymentRequest $request, $id)
*/
public function destroy(Request $request, $id)
{
+ $request->validate([
+ 'reason' => 'required'
+ ]);
+
$payment = Payment::findOrFail($id);
$payment->isAllowedToDelete();
- $response = $payment->requestCancel($request);
-
- if (! $response) {
- foreach ($payment->details as $paymentDetail) {
- if (! $paymentDetail->isDownPayment()) {
- $reference = $paymentDetail->referenceable;
- $reference->remaining += $payment->amount;
- $reference->save();
- $reference->form->done = false;
- $reference->form->save();
+ DB::connection('tenant')->transaction(function () use ($payment, $request) {
+ $payment->requestCancel($request);
+
+ // Kirim notifikasi by program & email
+ $superAdminRole = Role::where('name', 'super admin')->first();
+ $emailUsers = User::whereHas('roles', function (Builder $query) use ($superAdminRole) {
+ $query->where('role_id', '=', $superAdminRole->id);
+ })->get();
+
+ foreach ($emailUsers as $recipient) {
+ // create token based on request_approval_to
+ $token = Token::where('user_id', $recipient->id)->first();
+
+ if (!$token) {
+ $token = new Token([
+ 'user_id' => $recipient->id,
+ 'token' => md5($recipient->email . '' . now()),
+ ]);
+ $token->save();
}
+
+ Mail::to([
+ $recipient->email,
+ ])->queue(new PaymentCancellationApprovalRequest(
+ $payment,
+ $recipient,
+ $payment->form,
+ $token->token
+ ));
}
- }
+
+ // if (!$response) {
+ // foreach ($payment->details as $paymentDetail) {
+ // if (!$paymentDetail->isDownPayment()) {
+ // $reference = $paymentDetail->referenceable;
+ // $reference->remaining += $payment->amount;
+ // $reference->save();
+ // $reference->form->done = false;
+ // $reference->form->save();
+ // }
+ // }
+ // }
+ });
return response()->json([], 204);
}
+
+ public function getReferences(Request $request)
+ {
+ // Split request filter for each reference type
+ $paymentOrderRequest = new Request();
+ $downPaymentRequest = new Request();
+ $paymentOrderString = 'paymentorder';
+ $downPaymentString = 'downpayment';
+ foreach ($request->all() as $key => $value) {
+ if (in_array($key, ['limit', 'page'])) {
+ $paymentOrderRequest->merge([
+ $key => $value
+ ]);
+ $downPaymentRequest->merge([
+ $key => $value
+ ]);
+ continue;
+ }
+ $explodedKey = explode('_', $key);
+
+ switch ($explodedKey[0]) {
+ case $paymentOrderString:
+ $keyAttribute = substr($key, strlen($paymentOrderString) + 1); //+1 for _
+ $paymentOrderRequest->merge([
+ $keyAttribute => $value
+ ]);
+ break;
+
+ case $downPaymentString:
+ $keyAttribute = substr($key, strlen($downPaymentString) + 1); //+1 for _
+ $downPaymentRequest->merge([
+ $keyAttribute => $value
+ ]);
+ break;
+
+ default:
+ # code...
+ break;
+ }
+ }
+
+ $references = new Collection();
+
+ $paymentOrders = PaymentOrder::from(PaymentOrder::getTableName() . ' as ' . PaymentOrder::$alias)->eloquentFilter($paymentOrderRequest);
+ $paymentOrders = PaymentOrder::joins($paymentOrders, $paymentOrderRequest->get('join'))->get();
+ $references = $references->concat($paymentOrders);
+
+ $downPayments = PurchaseDownPayment::from(PurchaseDownPayment::getTableName() . ' as ' . PurchaseDownPayment::$alias)->eloquentFilter($downPaymentRequest);
+ $downPayments = PurchaseDownPayment::joins($downPayments, $downPaymentRequest->get('join'))->get();
+ $references = $references->concat($downPayments);
+
+ $references = $references->sortBy($request->get('sort_by'));
+ $paginatedReferences = paginate_collection($references, $request->get('limit'), $request->get('page'));
+
+ return new ApiCollection($paginatedReferences);
+ }
+
+ public function getPaymentables(Request $request)
+ {
+ $paymentables = Payment::from(Payment::getTableName() . ' as ' . Payment::$alias)
+ ->select(['paymentable_id', 'paymentable_type', 'paymentable_name'])
+ ->eloquentFilter($request);
+ $paymentables = pagination($paymentables, $request->get('limit'));
+
+ return new ApiCollection($paymentables);
+ }
}
diff --git a/app/Http/Requests/Finance/Payment/Payment/StorePaymentRequest.php b/app/Http/Requests/Finance/Payment/Payment/StorePaymentRequest.php
index 034cca592..38aef780a 100644
--- a/app/Http/Requests/Finance/Payment/Payment/StorePaymentRequest.php
+++ b/app/Http/Requests/Finance/Payment/Payment/StorePaymentRequest.php
@@ -2,8 +2,10 @@
namespace App\Http\Requests\Finance\Payment\Payment;
+use App\Exceptions\PointException;
use App\Http\Requests\ValidationRule;
use App\Model\Accounting\ChartOfAccount;
+use App\Model\Finance\CashAdvance\CashAdvance;
use App\Model\Finance\Payment\PaymentDetail;
use App\Model\Master\Allocation;
use Illuminate\Foundation\Http\FormRequest;
@@ -35,6 +37,7 @@ public function rules()
'paymentable_id' => 'required|integer|min:0',
'paymentable_type' => 'required|string',
'details' => 'required|array',
+ 'notes' => 'nullable|max:255'
];
$rulesPaymentDetail = [
@@ -43,8 +46,8 @@ public function rules()
'details.*.allocation_id' => ValidationRule::foreignKeyNullable(Allocation::getTableName()),
'details.*.referenceable_type' => [
function ($attribute, $value, $fail) {
- if (! PaymentDetail::referenceableIsValid($value)) {
- $fail($attribute.' is invalid');
+ if (!PaymentDetail::referenceableIsValid($value)) {
+ $fail($attribute . ' is invalid');
}
},
],
@@ -52,4 +55,34 @@ function ($attribute, $value, $fail) {
return array_merge($rulesForm, $rulesPayment, $rulesPaymentDetail);
}
+
+ public function withValidator($validator)
+ {
+ $validator->after(function ($validator) {
+ // Cash out/bank out
+ if (request()->get('disbursed') == 1) {
+ $amountCashAdvance = 0;
+ $needToPayFromAccount = request()->get('amount');
+
+ if ((request()->filled('cash_advance.id'))) {
+ $cashAdvance = CashAdvance::find(request()->get('cash_advance')['id']);
+ $amountCashAdvance = $cashAdvance->amount_remaining;
+ if ($amountCashAdvance > $needToPayFromAccount) {
+ // All covered by cash advance
+ $needToPayFromAccount = 0;
+ } else {
+ $needToPayFromAccount = request()->get('amount') - $amountCashAdvance;
+ }
+ }
+
+ if ($needToPayFromAccount > 0) {
+ // Check balance payment account
+ $balancePaymentAccount = ChartOfAccount::find(request()->get('payment_account_id'))->total(date('Y-m-d 23:59:59'));
+ if ($balancePaymentAccount < $needToPayFromAccount) {
+ throw new PointException('Balance is not enough');
+ }
+ }
+ }
+ });
+ }
}
diff --git a/app/Mail/Finance/Payment/PaymentCancellationApprovalRequest.php b/app/Mail/Finance/Payment/PaymentCancellationApprovalRequest.php
new file mode 100644
index 000000000..1f2f6b9e8
--- /dev/null
+++ b/app/Mail/Finance/Payment/PaymentCancellationApprovalRequest.php
@@ -0,0 +1,49 @@
+payment = $payment;
+ $this->approver = $approver;
+ $this->form = $form;
+ $this->token = $token;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ return $this->subject('Cancellation Approval Email')
+ ->view('emails.finance.payment.cancellation-approval', [
+ 'payment' => $this->payment,
+ 'approverId' => $this->approver->id,
+ 'fullName' => $this->approver->getFullNameAttribute(),
+ 'form' => $this->form,
+ 'token' => $this->token,
+ 'cashAdvancePayment' => $this->payment->cashAdvance
+ ]);
+ }
+}
diff --git a/app/Model/AllocationReport.php b/app/Model/AllocationReport.php
index 3c3698bc6..fe3b53347 100644
--- a/app/Model/AllocationReport.php
+++ b/app/Model/AllocationReport.php
@@ -20,4 +20,12 @@ public function form()
{
return $this->belongsTo(Form::class);
}
+
+ /**
+ * Get all of the owning formable models.
+ */
+ public function allocationable()
+ {
+ return $this->morphTo();
+ }
}
diff --git a/app/Model/Finance/CashAdvance/CashAdvancePayment.php b/app/Model/Finance/CashAdvance/CashAdvancePayment.php
new file mode 100644
index 000000000..e691eff5a
--- /dev/null
+++ b/app/Model/Finance/CashAdvance/CashAdvancePayment.php
@@ -0,0 +1,40 @@
+ 'double'
+ ];
+
+ public function cashAdvance()
+ {
+ return $this->belongsTo(CashAdvance::class);
+ }
+
+ public function payment()
+ {
+ return $this->belongsTo(Payment::class);
+ }
+}
diff --git a/app/Model/Finance/Payment/Payment.php b/app/Model/Finance/Payment/Payment.php
index 13099fe38..6cff1f1b4 100644
--- a/app/Model/Finance/Payment/Payment.php
+++ b/app/Model/Finance/Payment/Payment.php
@@ -4,9 +4,12 @@
use App\Exceptions\BranchNullException;
use App\Exceptions\PointException;
+use App\Exceptions\UnauthorizedException;
use App\Model\Accounting\Journal;
+use App\Model\AllocationReport;
use App\Model\Finance\PaymentOrder\PaymentOrder;
use App\Model\Finance\CashAdvance\CashAdvance;
+use App\Model\Finance\CashAdvance\CashAdvancePayment;
use App\Model\Form;
use App\Model\Purchase\PurchaseDownPayment\PurchaseDownPayment;
use App\Model\Sales\PaymentCollection\PaymentCollection;
@@ -14,7 +17,6 @@
use App\Model\TransactionModel;
use App\Traits\Model\Finance\PaymentJoin;
use App\Traits\Model\Finance\PaymentRelation;
-use DB;
class Payment extends TransactionModel
{
@@ -48,6 +50,25 @@ public function isAllowedToUpdate()
public function isAllowedToDelete()
{
// TODO isAllowed to delete?
+
+ // Forbidden to delete,
+ // Jika memiliki permission
+ $this->isHaveAccessToDelete();
+
+ // Jika pada periode yang akan didelete sudah dilakukan close book maka akan mengirimkan pesan eror
+ // Wait for next release feature
+ }
+
+ // Check if auth user have access to delete payment
+ public function isHaveAccessToDelete()
+ {
+ $authUserId = auth()->user()->id;
+ // Only super admin & approver referenceable can delete
+ $isSuperAdmin = tenant($authUserId)->hasRole('super admin');
+ $isApproverReferenceable = $this->details()->first()->referenceable->form->approval_by;
+ if ((!$isSuperAdmin) && ($authUserId != $isApproverReferenceable)) {
+ throw new UnauthorizedException();
+ }
}
public static function create($data)
@@ -58,7 +79,7 @@ public static function create($data)
$payment->paymentable_name = $data['paymentable_name'] ?? $payment->paymentable->name;
$paymentDetails = self::mapPaymentDetails($data);
-
+
// Reference Payment Order
if (isset($data['referenceable_type']) && $data['referenceable_type'] == 'PaymentOrder') {
$paymentOrder = PaymentOrder::find($data['referenceable_id']);
@@ -151,6 +172,38 @@ public static function create($data)
);
$form->save();
+ $payByCashAdvance = 0;
+ // If user select cash advance
+ if (isset($data['cash_advance']['id']) && $data['cash_advance']['id'] != null) {
+ $cashAdvance = CashAdvance::find($data['cash_advance']['id']);
+
+ $payByCashAdvance = $cashAdvance->amount_remaining;
+
+ if ($cashAdvance->amount_remaining > $payment->amount) {
+ $payByCashAdvance = $payment->amount;
+ $cashAdvance->amount_remaining = $cashAdvance->amount_remaining - $payment->amount;
+ }
+
+ CashAdvancePayment::create([
+ 'cash_advance_id' => $cashAdvance->id,
+ 'payment_id' => $payment->id,
+ 'amount' => $payByCashAdvance
+ ]);
+
+ if ($cashAdvance->amount_remaining == 0 || $data['cash_advance']['close'] == true) {
+ $cashAdvance->amount_remaining = 0;
+ $cashAdvance->form->done = 1;
+ $cashAdvance->form->save();
+ }
+ $cashAdvance->save();
+
+ $data['activity'] = ucfirst(strtolower($cashAdvance->payment_type)) . ' Out Withdrawal (' . $form->number . ')';
+ CashAdvance::mapHistory($cashAdvance, $data);
+ }
+
+ // Save allocation reports
+ self::mapAllocationReports($data, $payment);
+
// Reference Cash Advance
if (isset($data['referenceable_type']) && $data['referenceable_type'] == 'CashAdvance') {
$cashAdvance = CashAdvance::find($data['referenceable_id']);
@@ -159,13 +212,13 @@ public static function create($data)
}
$cashAdvance->payments()->attach($payment->id);
$cashAdvance->amount_remaining = $cashAdvance->amount_remaining - $payment->amount;
- if($cashAdvance->amount_remaining == 0) {
+ if ($cashAdvance->amount_remaining == 0) {
$cashAdvance->form->done = 1;
$cashAdvance->form->save();
}
$cashAdvance->save();
- $data['activity'] = ucfirst(strtolower($cashAdvance->payment_type)).' Out Withdrawal ('.$form->number.')';
+ $data['activity'] = ucfirst(strtolower($cashAdvance->payment_type)) . ' Out Withdrawal (' . $form->number . ')';
CashAdvance::mapHistory($cashAdvance, $data);
}
@@ -173,7 +226,7 @@ public static function create($data)
if ($isPaymentCollection) {
self::updateJournalPaymentCollection($payment, $journalsPaymentCollection);
} else {
- error_log('false');
+ // error_log('false');
self::updateJournal($payment);
}
@@ -192,6 +245,22 @@ private static function mapPaymentDetails($data)
}, $data['details']);
}
+ private static function mapAllocationReports($data, $payment)
+ {
+ return array_map(function ($detail) use ($data, $payment) {
+ if (isset($detail['allocation_id'])) {
+ $allocationReport = new AllocationReport;
+ $allocationReport->allocation_id = $detail['allocation_id'];
+ $allocationReport->allocationable_id = $payment->id;
+ $allocationReport->allocationable_type = $payment::$morphName;
+ $allocationReport->form_id = $payment->form->id;
+ $allocationReport->notes = $detail['notes'];
+
+ $allocationReport->save();
+ }
+ }, $data['details']);
+ }
+
private static function calculateAmount($paymentDetails)
{
return array_reduce($paymentDetails, function ($carry, $detail) {
@@ -209,12 +278,12 @@ private static function mapPaymentCollectionJournals($payment, $data)
$paymentDetail->fill($detail);
$paymentDetail->referenceable_type = $data['referenceable_type'] ?? null;
$paymentDetail->referenceable_id = $data['referenceable_id'] ?? null;
-
+
$journal = new Journal;
$journal->form_id_reference = optional(optional($paymentDetail->referenceable)->form)->id;
$journal->notes = $paymentDetail->notes;
$journal->chart_of_account_id = $paymentDetail->chart_of_account_id;
-
+
if ($detail['payment_collection_type'] === 'SalesDownPayment') {
$journal->debit = $paymentDetail->amount;
$journals['debit'][] = $journal;
@@ -271,19 +340,19 @@ private static function generateFormNumber($payment, $number, $increment)
*/
private static function getLastPaymentIncrement($payment, $incrementGroup)
{
- $lastPayment = self::from(self::getTableName().' as '.self::$alias)
+ $lastPayment = self::from(self::getTableName() . ' as ' . self::$alias)
->joinForm()
->where('form.increment_group', $incrementGroup)
->whereNotNull('form.number')
- ->where(self::$alias.'.payment_type', $payment->payment_type)
- ->where(self::$alias.'.disbursed', $payment->disbursed)
+ ->where(self::$alias . '.payment_type', $payment->payment_type)
+ ->where(self::$alias . '.disbursed', $payment->disbursed)
->with('form')
->orderBy('form.increment', 'desc')
- ->select(self::$alias.'.*')
+ ->select(self::$alias . '.*')
->first();
$increment = 1;
- if (! empty($lastPayment)) {
+ if (!empty($lastPayment)) {
$increment += $lastPayment->form->increment;
}
@@ -309,7 +378,7 @@ private static function updateJournal($payment)
$journal->journalable_type = $payment->paymentable_type;
$journal->journalable_id = $payment->paymentable_id;
$journal->chart_of_account_id = $payment->payment_account_id;
- if (! $payment->disbursed) {
+ if (!$payment->disbursed) {
$journal->debit = $payment->amount;
} else {
$journal->credit = $payment->amount;
@@ -324,7 +393,7 @@ private static function updateJournal($payment)
$journal->journalable_id = $payment->paymentable_id;
$journal->notes = $paymentDetail->notes;
$journal->chart_of_account_id = $paymentDetail->chart_of_account_id;
- if (! $payment->disbursed) {
+ if (!$payment->disbursed) {
$journal->credit = $paymentDetail->amount;
} else {
$journal->debit = $paymentDetail->amount;
@@ -355,6 +424,6 @@ private static function updateJournalPaymentCollection($payment, $journalsPaymen
$journal->journalable_type = $payment->paymentable_type;
$journal->journalable_id = $payment->paymentable_id;
$journal->save();
- }
+ }
}
}
diff --git a/app/Traits/Model/Finance/PaymentJoin.php b/app/Traits/Model/Finance/PaymentJoin.php
index b180e0841..56e149471 100644
--- a/app/Traits/Model/Finance/PaymentJoin.php
+++ b/app/Traits/Model/Finance/PaymentJoin.php
@@ -59,6 +59,10 @@ public static function joins($query, $joins)
});
}
+ if (in_array('cashAdvances', $joins)) {
+ $query = $query->with(['cashAdvances']);
+ }
+
return $query;
}
}
diff --git a/app/Traits/Model/Finance/PaymentRelation.php b/app/Traits/Model/Finance/PaymentRelation.php
index 34c727f9c..dfd252c28 100644
--- a/app/Traits/Model/Finance/PaymentRelation.php
+++ b/app/Traits/Model/Finance/PaymentRelation.php
@@ -3,6 +3,9 @@
namespace App\Traits\Model\Finance;
use App\Model\Accounting\ChartOfAccount;
+use App\Model\AllocationReport;
+use App\Model\Finance\CashAdvance\CashAdvance;
+use App\Model\Finance\CashAdvance\CashAdvancePayment;
use App\Model\Finance\Payment\PaymentDetail;
use App\Model\Form;
use App\Model\Purchase\PurchaseOrder\PurchaseOrder;
@@ -45,4 +48,22 @@ public function purchaseOrders()
->orWhere(Form::getTableName('cancellation_status'), '!=', '1');
});
}
+
+ /**
+ * Get all related cash advances
+ */
+ public function cashAdvances()
+ {
+ return $this->belongsToMany(CashAdvance::class, 'cash_advance_payment', 'payment_id', 'cash_advance_id');
+ }
+
+ public function cashAdvance()
+ {
+ return $this->hasOne(CashAdvancePayment::class);
+ }
+
+ public function allocationReports()
+ {
+ return $this->morphMany(AllocationReport::class, 'allocationable');
+ }
}
diff --git a/app/helpers.php b/app/helpers.php
index 3c6913b2a..2b2bd7d54 100644
--- a/app/helpers.php
+++ b/app/helpers.php
@@ -2,6 +2,9 @@
use App\Model\SettingJournal;
use Carbon\Carbon;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Pagination\Paginator;
+use Illuminate\Support\Collection;
use Illuminate\Support\Str;
if (! function_exists('log_object')) {
@@ -363,3 +366,26 @@ function response_error($error)
return response (['code' => $code, 'message' => $message], $httpCode);
}
}
+
+if (! function_exists('paginate_collection')) {
+ /**
+ * Paginate collection (not from query).
+ *
+ * @param $collection
+ * @param null $limit
+ * @return string
+ */
+ function paginate_collection($collection, $limit = null, $page = null, $options = [])
+ {
+ if (! $limit) {
+ return $collection->paginate(100);
+ }
+
+ // limit call maximum 1000 item per page
+ $limit = $limit > 1000 ? 1000 : $limit;
+
+ $page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
+ $collection = $collection instanceof Collection ? $collection : Collection::make($collection);
+ return new LengthAwarePaginator($collection->forPage($page, $limit), $collection->count(), $limit, $page, $options);
+ }
+}
\ No newline at end of file
diff --git a/database/migrations/tenant/2022_11_04_052031_alter_cash_advance_payment_add_amount_column.php b/database/migrations/tenant/2022_11_04_052031_alter_cash_advance_payment_add_amount_column.php
new file mode 100644
index 000000000..100da4556
--- /dev/null
+++ b/database/migrations/tenant/2022_11_04_052031_alter_cash_advance_payment_add_amount_column.php
@@ -0,0 +1,32 @@
+unsignedDecimal('amount', 65, 30);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('cash_advance_payment', function (Blueprint $table) {
+ $table->dropColumn(['amount']);
+ });
+ }
+}
diff --git a/resources/views/emails/finance/payment/cancellation-approval.blade.php b/resources/views/emails/finance/payment/cancellation-approval.blade.php
new file mode 100644
index 000000000..8695e3e49
--- /dev/null
+++ b/resources/views/emails/finance/payment/cancellation-approval.blade.php
@@ -0,0 +1,107 @@
+@extends('emails.template')
+
+@section('content')
+
Cancellation Approval Email
+
+
+ Hello Mrs/Mr/Ms {{ $fullName }},
+
+ You Have an approval for Payment Cancellation. we would like to details as follows:
+
+
+
+
+
+ | Form Number |
+ : {{ $payment->form->number ?: '-' }} |
+
+
+ | Form Date |
+ : {{ date('d F Y', strtotime($payment->form->date)) ?: '-' }} |
+
+
+ | Form Reference |
+ : {{ $payment->details()->first()->referenceable->form->number ?: '-' }} |
+
+
+ | Cash Advance |
+ : {{ $cashAdvancePayment ? $cashAdvancePayment->cashAdvance->form->number : '-' }} |
+
+
+ | Amount Cash Advance |
+ : {{ $cashAdvancePayment ? $cashAdvancePayment->amount : '-' }} |
+
+
+ | Cash Account |
+ : {{ $payment->paymentAccount->label }} |
+
+
+ | Person |
+ : {{ $payment->paymentable_name }} |
+
+
+ | Notes |
+ : {{ $payment->form->notes ?: '-' }} |
+
+
+
+
+
+
+
+
+ | No |
+ Account |
+ Notes |
+ Amount |
+ Allocation |
+
+
+
+ @foreach($payment->details as $i => $detail)
+
+ |
+ {{ ++$i }}
+ |
+
+ {{ $detail->chartOfAccount->label }}
+ |
+
+ {{ $detail->notes }}
+ |
+
+ {{ $detail->amount }}
+ |
+
+ {{ $detail->allocation ? $detail->allocation->name : '-' }}
+ |
+
+ @php ($i++)
+ @endforeach
+
+
+
+
+
+
+@stop
\ No newline at end of file
diff --git a/routes/api/finance.php b/routes/api/finance.php
index cfb3a6488..dc18b4d2e 100644
--- a/routes/api/finance.php
+++ b/routes/api/finance.php
@@ -1,6 +1,12 @@
namespace('Finance')->group(function () {
+ Route::prefix('payments')->namespace('Payment')->group(function (){
+ Route::get('get-paymentables', 'PaymentController@getPaymentables');
+ Route::get('get-references', 'PaymentController@getReferences');
+ Route::post('{id}/cancellation-approve', 'PaymentCancellationApprovalController@approve');
+ Route::post('{id}/cancellation-reject', 'PaymentCancellationApprovalController@reject');
+ });
Route::apiResource('payments', 'Payment\\PaymentController');
Route::post('payment-orders/{id}/approve', 'Payment\\PaymentOrderApprovalController@approve');
Route::post('payment-orders/{id}/reject', 'Payment\\PaymentOrderApprovalController@reject');
diff --git a/tests/Feature/Http/Finance/Cash/CashOutCancellationTest.php b/tests/Feature/Http/Finance/Cash/CashOutCancellationTest.php
new file mode 100644
index 000000000..2c3b69e7d
--- /dev/null
+++ b/tests/Feature/Http/Finance/Cash/CashOutCancellationTest.php
@@ -0,0 +1,95 @@
+createPaymentOrder();
+ $data = $this->getDataPayment($paymentOrder);
+
+ $this->json('POST', self::$path . '/payments', $data, $this->headers);
+
+ return Payment::orderBy('id', 'desc')->first();
+ }
+
+ // Success request to delete
+ /** @test */
+ public function success_request_to_delete()
+ {
+ $data = [
+ 'reason' => 'Please delete this form because...'
+ ];
+
+ $payment = $this->createPayment();
+
+ $response = $this->json('DELETE', self::$path . '/payments/' . $payment->id, $data, $this->headers);
+ $response->assertStatus(204);
+ }
+
+ // Fail request to delete because didn't insert reason
+ /** @test */
+ public function fail_request_to_delete()
+ {
+ $payment = $this->createPayment();
+
+ $response = $this->json('DELETE', self::$path . '/payments/' . $payment->id, $this->headers);
+ $response->assertStatus(422);
+ }
+
+ public function getPaymentReadyToCancellation()
+ {
+ $data = [
+ 'reason' => 'Please delete this form because...'
+ ];
+
+ $payment = $this->createPayment();
+ $this->json('DELETE', self::$path . '/payments/' . $payment->id, $data, $this->headers);
+
+ return Payment::orderBy('id', 'desc')->first();
+ }
+
+ // Success approve to delete
+ /** @test */
+ public function success_approve_to_delete()
+ {
+ $payment = $this->getPaymentReadyToCancellation();
+
+ $response = $this->json('POST', self::$path . '/payments/' . $payment->id . '/cancellation-approve', $this->headers);
+ $response->assertStatus(200);
+ }
+
+ // Success reject to delete
+ /** @test */
+ public function success_reject_to_delete()
+ {
+ $data = [
+ 'reason' => 'Sorry, this form can\'t be deleted'
+ ];
+
+ $payment = $this->getPaymentReadyToCancellation();
+
+ $response = $this->json('POST', self::$path . '/payments/' . $payment->id . '/cancellation-reject', $data, $this->headers);
+ $response->assertStatus(200);
+ }
+
+ // Fail reject to delete
+ /** @test */
+ public function fail_reject_to_delete()
+ {
+ $payment = $this->getPaymentReadyToCancellation();
+
+ $response = $this->json('POST', self::$path . '/payments/' . $payment->id . '/cancellation-reject', $this->headers);
+ $response->assertStatus(422);
+ }
+}
diff --git a/tests/Feature/Http/Finance/Cash/CashOutSetup.php b/tests/Feature/Http/Finance/Cash/CashOutSetup.php
new file mode 100644
index 000000000..4f13190b5
--- /dev/null
+++ b/tests/Feature/Http/Finance/Cash/CashOutSetup.php
@@ -0,0 +1,234 @@
+signIn();
+ $this->setRole();
+ $this->setProject();
+ $this->importChartOfAccount();
+ }
+
+ private function importChartOfAccount()
+ {
+ Excel::import(new ChartOfAccountImport(), storage_path('template/chart_of_accounts_manufacture.xlsx'));
+
+ $this->artisan('db:seed', [
+ '--database' => 'tenant',
+ '--class' => 'SettingJournalSeeder',
+ '--force' => true,
+ ]);
+ }
+
+ public function getChartOfAccountCash()
+ {
+ return ChartOfAccount::whereHas('type', function ($query) {
+ $query->where('name', 'CASH');
+ })->first();
+ }
+
+ public function createPaymentOrder($onlyGetData = false)
+ {
+ $countDetails = rand(1, 5);
+
+ $coas = ChartOfAccount::whereHas('type', function ($query) {
+ $query->whereIn('name', ['DIRECT EXPENSE', 'OTHER EXPENSE', 'OTHER CURRENT ASSET', 'INCOME TAX RECEIVABLE', 'INCOME TAX PAYABLE', 'OTHER ACCOUNT RECEIVABLE', 'OTHER CURRENT LIABILITY', 'LIABILITAS JANGKA PANJANG', 'FACTORY OVERHEAD COST']);
+ })->inRandomOrder()->limit($countDetails)->get();
+ $details = [];
+ for ($i = 0; $i < $countDetails; $i++) {
+ ${'coa' . $i} = $coas->skip($i)->first();
+ $details[$i] = [
+ 'chart_of_account_id' => ${'coa' . $i}->id,
+ 'chart_of_account_name' => ${'coa' . $i}->label,
+ 'amount' => rand(1, 100) * 1_000,
+ 'allocation_id' => factory(Allocation::class)->create()->id
+ ];
+ }
+
+ $paymentable = factory(Customer::class)->create();
+ $form = [
+ 'payment_type' => 'cash',
+ 'due_date' => Carbon::create(date('Y'), date('m'), rand(1, 28)),
+ 'paymentable_id' => $paymentable->id,
+ 'paymentable_type' => $paymentable::$morphName,
+ 'details' => $details
+ ];
+
+ if ($onlyGetData) {
+ return $form;
+ }
+ $paymentOrder = PaymentOrder::create($form);
+ // Approve payment order
+ $paymentOrder->form->approval_by = auth()->user()->id;
+ $paymentOrder->form->save();
+
+ return $paymentOrder;
+ }
+
+ public function createCashAdvance($amount)
+ {
+ // Copied & adjusted from CashAdvanceTest/createDataCashAdvance & CashAdvanceTest/makePaymentCashIn
+ $account = ChartOfAccount::where('name', 'CASH')->first();
+ $account_detail = ChartOfAccount::where('name', 'OTHER INCOME')->first();
+
+ $employee = factory(Employee::class)->create();
+ $user = factory(User::class)->create();
+
+ // s: insert cash in
+ $amount_account = rand(10, 100) * 100_000;
+ $data = [
+ 'increment_group' => date('Ym'),
+ 'date' => date('Y-m-d H:i:s'),
+ 'due_date' => date('Y-m-d H:i:s'),
+ 'payment_type' => "cash",
+ 'payment_account_id' => $account->id,
+ 'paymentable_id' => $employee->id,
+ 'paymentable_name' => $employee->name,
+ 'paymentable_type' => Employee::$morphName,
+ 'disbursed' => false,
+ 'notes' => null,
+ 'amount' => $amount_account,
+ 'details' => array(
+ [
+ 'chart_of_account_id' => $account_detail->id,
+ 'amount' => $amount_account,
+ 'allocation_id' => null,
+ 'allocation_name' => null,
+ 'notes' => "Kas"
+ ]
+ )
+ ];
+ $payment = Payment::create($data);
+
+ $form = $payment->form;
+ $journal = new Journal;
+ $journal->form_id = $form->id;
+ $journal->chart_of_account_id = $account->id;
+ $journal->debit = $amount_account;
+ $journal->save();
+
+ //create sample cash advance
+ $data = [
+ 'increment_group' => date('Ym'),
+ 'date' => date('Y-m-d H:i:s'),
+ 'payment_type' => 'cash',
+ 'employee_id' => $employee->id,
+ 'request_approval_to' => $user->id,
+ 'notes' => 'Notes Form',
+ 'amount' => $amount,
+ 'activity' => 'Created',
+ 'details' => array(
+ [
+ 'chart_of_account_id' => $account->id,
+ 'amount' => $amount,
+ 'notes' => 'Notes'
+ ]
+ )
+ ];
+
+ return CashAdvance::create($data);
+ }
+
+ public function transformPaymentOrderDetails($paymentOrder)
+ {
+ return $paymentOrder->details->transform(function ($detail) {
+ return [
+ 'allocation_id' => $detail->allocation_id,
+ 'amount' => $detail->amount,
+ 'chart_of_account_id' => $detail->chart_of_account_id,
+ 'notes' => $detail->notes
+ ];
+ });
+ }
+
+ public function paymentAssertDatabaseHas($response, $data)
+ {
+ $this->assertDatabaseHas('payments', [
+ 'id' => $response->json('data.id'),
+ 'payment_type' => "CASH"
+ ], 'tenant')
+ ->assertDatabaseHas('forms', [
+ 'id' => $response->json('data.form.id')
+ ], 'tenant');
+
+ foreach ($data['details'] as $detail) {
+ $this->assertDatabaseHas('payment_details', $detail, 'tenant');
+ }
+ }
+
+ public function getDataPayment($reference)
+ {
+ $paymentAccount = $this->getChartOfAccountCash();
+ $this->makePaymentCashIn($paymentAccount, $reference->amount);
+ $details = [];
+ if ($reference::$morphName == 'PaymentOrder') {
+ $details = $this->transformPaymentOrderDetails($reference);
+ }
+
+ return [
+ 'amount' => $reference->amount,
+ 'payment_type' => $reference->payment_type,
+ 'date' => date('Y-m-d H:i:s'),
+ 'increment_group' => date('Ym'),
+ 'payment_account_id' => $paymentAccount->id,
+ 'disbursed' => true,
+ 'paymentable_id' => $reference->paymentable_id,
+ 'paymentable_type' => $reference->paymentable_type,
+ 'referenceable_type' => $reference::$morphName,
+ 'referenceable_id' => $reference->id,
+ 'details' => $details,
+ ];
+ }
+
+ // Copied from CashAdvanceTest
+ public function makePaymentCashIn($account, $amount_account)
+ {
+ $paymentable = factory(Customer::class)->create();
+ $account_detail = ChartOfAccount::where('name', 'OTHER INCOME')->first();
+ // s: insert cash in
+ $data = [
+ 'increment_group' => date('Ym'),
+ 'date' => date('Y-m-d H:i:s'),
+ 'due_date' => date('Y-m-d H:i:s'),
+ 'payment_type' => "cash",
+ 'payment_account_id' => $account->id,
+ 'paymentable_id' => $paymentable->id,
+ 'paymentable_name' => $paymentable->name,
+ 'paymentable_type' => $paymentable::$morphName,
+ 'disbursed' => false,
+ 'notes' => null,
+ 'amount' => $amount_account,
+ 'details' => array(
+ [
+ 'chart_of_account_id' => $account_detail->id,
+ 'amount' => $amount_account,
+ 'allocation_id' => null,
+ 'allocation_name' => null,
+ 'notes' => "Kas"
+ ]
+ )
+ ];
+
+ Payment::create($data);
+ // e: insert cash in
+ }
+}
diff --git a/tests/Feature/Http/Finance/Cash/CashOutTest.php b/tests/Feature/Http/Finance/Cash/CashOutTest.php
new file mode 100644
index 000000000..14025833c
--- /dev/null
+++ b/tests/Feature/Http/Finance/Cash/CashOutTest.php
@@ -0,0 +1,108 @@
+success_cash_out_from_payment_order_with_cash_advance_and_account();
+
+ $response = $this->json('GET', self::$path . '/payments?join=form,payment_account,details,account,allocation&sort_by=-form.date&fields=payment.*&filter_form=notArchived%3Bnull&filter_like=%7B%7D&filter_equal=%7B%22payment.payment_type%22:%22cash%22%7D&filter_date_min=%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%3Bdetails.chartOfAccount%3Bdetails.allocation%3Bpaymentable&page=1', $this->headers);
+ $response->assertStatus(200);
+ }
+
+ // Test success get a cash out
+ /** @test */
+ public function success_get_a_cash_out()
+ {
+ $this->success_cash_out_from_payment_order_without_cash_advance();
+ $payment = Payment::orderBy('id', 'desc')->first();
+ $data = [
+ 'includes' => 'form.branch;paymentAccount;details.chartOfAccount;details.allocation'
+ ];
+ $response = $this->json('GET', self::$path . '/payments/' . $payment->id, $data, $this->headers);
+ $response->assertStatus(200);
+ }
+
+ // Test create payment order for reference cash out
+ /** @test */
+ public function success_create_payment_order()
+ {
+ $data = $this->createPaymentOrder(true);
+ $response = $this->json('POST', self::$path . '/payment-orders', $data, $this->headers);
+ $response->assertStatus(201);
+
+ $this->assertDatabaseHas('payment_orders', [
+ 'id' => $response->json('data.id')
+ ], 'tenant')
+ ->assertDatabaseHas('forms', [
+ 'id' => $response->json('data.form.id')
+ ], 'tenant');
+
+ foreach ($data['details'] as $detail) {
+ unset($detail['chart_of_account_name']);
+ $this->assertDatabaseHas('payment_order_details', $detail, 'tenant');
+ }
+ }
+
+ // Test cash out from payment order without cash advance
+ /** @test */
+ public function success_cash_out_from_payment_order_without_cash_advance()
+ {
+ $paymentOrder = $this->createPaymentOrder();
+ $data = $this->getDataPayment($paymentOrder);
+
+ $response = $this->json('POST', self::$path . '/payments', $data, $this->headers);
+ $response->assertStatus(201);
+
+ $this->paymentAssertDatabaseHas($response, $data);
+ }
+
+ // Test cash out from payment order with cash advance and account
+ /** @test */
+ public function success_cash_out_from_payment_order_with_cash_advance_and_account()
+ {
+ $paymentOrder = $this->createPaymentOrder();
+ $data = $this->getDataPayment($paymentOrder);
+ $cashAdvance = $this->createCashAdvance($paymentOrder->amount - 10_000);
+
+ $data['cash_advance'] = [
+ 'id' => $cashAdvance->id,
+ 'close' => false
+ ];
+
+ $response = $this->json('POST', self::$path . '/payments', $data, $this->headers);
+ $response->assertStatus(201);
+
+ $this->paymentAssertDatabaseHas($response, $data);
+ }
+
+ // Test cash out from payment order with cash advance only
+ /** @test */
+ public function success_cash_out_from_payment_order_with_cash_advance_only()
+ {
+ $paymentOrder = $this->createPaymentOrder();
+ $data = $this->getDataPayment($paymentOrder);
+ $cashAdvance = $this->createCashAdvance($paymentOrder->amount);
+
+ $data['cash_advance'] = [
+ 'id' => $cashAdvance->id,
+ 'close' => false
+ ];
+
+ $response = $this->json('POST', self::$path . '/payments', $data, $this->headers);
+ $response->assertStatus(201);
+
+ $this->paymentAssertDatabaseHas($response, $data);
+ }
+}