Merge branch 'dillon/90-e-invoice-f' into vapor/development

This commit is contained in:
Dillon Ngo
2025-10-02 13:36:17 +08:00
10 changed files with 202 additions and 56 deletions
@@ -15,6 +15,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Booking;
use App\Models\Transaction;
use Illuminate\Support\Facades\Log;
@@ -71,19 +72,29 @@ class ProcessCreditNoteReportV2CommandJob implements ShouldQueue
'EInvoiceValidationLink' => $eInvoiceValidationLink,
]);
$booking = Booking::where('marking', $ref)->first();
if($booking){
// $booking = Booking::where('marking', $ref)->first();
// if($booking){
// if($docNo != "" && $docNo != "<<New>>"){
// $payments = $booking->transactions()->payments()->get();
// foreach ($payments as $payment) {
// $refundTransaction = $payment->transactions()->refunds()->whereIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED])->latest()->first();
// if($refundTransaction){
// $this->updateOrCreateKeyValuePair($refundTransaction, KVPKey::AUTOCOUNT_DOCNO_CREDIT_NOTE, $docNo);
// if($eInvoiceValidationLink){
// $this->updateOrCreateKeyValuePair($refundTransaction, KVPKey::AUTOCOUNT_EINVOICE_VALIDATION_LINK_CREDIT_NOTE, $eInvoiceValidationLink);
// }
// break;
// }
// }
// }
// }
$transaction = Transaction::where('bill_no', $ref)->first();
if($transaction){
if($docNo != "" && $docNo != "<<New>>"){
$payments = $booking->transactions()->payments()->get();
foreach ($payments as $payment) {
$refundTransaction = $payment->transactions()->refunds()->whereIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED])->latest()->first();
if($refundTransaction){
$this->updateOrCreateKeyValuePair($refundTransaction, KVPKey::AUTOCOUNT_DOCNO_CREDIT_NOTE, $docNo);
if($eInvoiceValidationLink){
$this->updateOrCreateKeyValuePair($refundTransaction, KVPKey::AUTOCOUNT_EINVOICE_VALIDATION_LINK_CREDIT_NOTE, $eInvoiceValidationLink);
}
break;
}
$this->updateOrCreateKeyValuePair($transaction, KVPKey::AUTOCOUNT_DOCNO_CREDIT_NOTE, $docNo);
if($eInvoiceValidationLink){
$this->updateOrCreateKeyValuePair($transaction, KVPKey::AUTOCOUNT_EINVOICE_VALIDATION_LINK_CREDIT_NOTE, $eInvoiceValidationLink);
}
}
}
@@ -79,7 +79,7 @@ class ExportsARCreditNoteReport implements FromQuery, WithHeadings, WithHeadingR
$refundRemarkGroupAccNo = null;
$company = $transaction->owner->owner;
$kvps = KeyValuePair::where('value', $transaction->id)
->where('key', 'App\Models\Transaction')
->where('key', KVPKey::TRANSACTION_MODEL_CLASS)
->orderByDesc('created_at')
->get();
@@ -119,7 +119,7 @@ class ExportsARCreditNoteReport implements FromQuery, WithHeadings, WithHeadingR
'<<New>>', //DocNo
$formattedDocumentDate, //DocDate
$company->debtor, //DebtorCode
$booking ? $booking->marking : '', //Ref
$transaction->bill_no ?? '', //Ref
$refundRemarkGroup ?? '', //Description
$refundRemarkGroup ?? '', //Reason
'C', //DeptNo
@@ -94,26 +94,43 @@ class GenerateCreditNotePdfV2Logic
if($eInvoiceStarted) {
$eInvoiceStarted = false; //reset to re-evaluate second time
$kvp = KeyValuePair::where('key', KVPKey::TRANSACTION_MODEL_CLASS)->where('value', $transaction->id)->first();
$refundTransaction = $kvp ? $kvp->owner : null;
if($refundTransaction && $refundTransaction->type === TransactionType::REFUND){
$metadata = $refundTransaction->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO_CREDIT_NOTE)->first();
//OLD BUSINESS LOGIC
// $kvp = KeyValuePair::where('key', KVPKey::TRANSACTION_MODEL_CLASS)->where('value', $transaction->id)->first();
// $refundTransaction = $kvp ? $kvp->owner : null;
// if($refundTransaction && $refundTransaction->type === TransactionType::REFUND){
// $metadata = $refundTransaction->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO_CREDIT_NOTE)->first();
// if($metadata){
// $autoCountInvoiceId = $metadata->value;
// }
// $metadata = $refundTransaction->attributesKVP()->where('key', KVPKey::AUTOCOUNT_EINVOICE_VALIDATION_LINK_CREDIT_NOTE)->first();
// if($metadata){
// $autoCountEInvoiceValidationLink = $metadata->value;
// }
// if($autoCountInvoiceId && $autoCountEInvoiceValidationLink){
// $eInvoiceStarted = true;
// }
// }
if($transaction && $transaction->type === TransactionType::CREDIT_NOTE){
$metadata = $transaction->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO_CREDIT_NOTE)->first();
if($metadata){
$autoCountInvoiceId = $metadata->value;
}
$metadata = $refundTransaction->attributesKVP()->where('key', KVPKey::AUTOCOUNT_EINVOICE_VALIDATION_LINK_CREDIT_NOTE)->first();
if($metadata){
$autoCountEInvoiceValidationLink = $metadata->value;
}
$metadata = $transaction->attributesKVP()->where('key', KVPKey::AUTOCOUNT_EINVOICE_VALIDATION_LINK_CREDIT_NOTE)->first();
if($metadata){
$autoCountEInvoiceValidationLink = $metadata->value;
}
Log::info('AUTOCOUNT_DOCNO_CREDIT_NOTE: ' . $autoCountInvoiceId);
Log::info('AUTOCOUNT_EINVOICE_VALIDATION_LINK_CREDIT_NOTE: ' . $autoCountEInvoiceValidationLink);
if($autoCountInvoiceId && $autoCountEInvoiceValidationLink){
$eInvoiceStarted = true;
}
}
else{
if($autoCountInvoiceId && $autoCountEInvoiceValidationLink){
$eInvoiceStarted = true;
}
}
}
Log::info('AUTOCOUNT_DOCNO_CREDIT_NOTE: ' . $autoCountInvoiceId);
Log::info('AUTOCOUNT_EINVOICE_VALIDATION_LINK_CREDIT_NOTE: ' . $autoCountEInvoiceValidationLink);
if($eInvoiceStarted)
{
@@ -111,10 +111,10 @@ class CreditWalletProcessor
$wallet = $this->updatesWallet->execute($wallet, $walletObject);
if($relatedTransaction && $relatedTransaction instanceof Transaction){
$kvp = $relatedTransaction->attributesKVP()->where('key', 'App\Models\Transaction')->where('value', $transaction->id)->latest()->first();
$kvp = $relatedTransaction->attributesKVP()->where('key', KVPKey::TRANSACTION_MODEL_CLASS)->where('value', $transaction->id)->latest()->first();
if(!$kvp){
$keyValuePairObject = new KeyValuePairObject(
"App\Models\Transaction",
KVPKey::TRANSACTION_MODEL_CLASS,
$transaction->id
);
$this->createsKeyValuePair->execute($relatedTransaction, $keyValuePairObject);
@@ -0,0 +1,110 @@
<?php
namespace App\Console\Commands;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Models\Booking;
use App\Models\LarkPushed;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class LarkPushBookingModuleCommand extends Command
{
protected $signature = 'lark:push-booking-module {--limit=10}';
protected $description = 'Send booking records to a Lark webhook, one by one.';
public function handle(): int
{
$module = 'Booking';
$webhookId = config('services.lark.booking_webhook_id', env('LARK_BOOKING1_WEBHOOK_ID'));
if (!$webhookId) {
$this->error('[Lark ' . $module . '] Missing LARK_BOOKING1_WEBHOOK_ID');
return self::FAILURE;
}
if (!app()->isProduction()) {
$this->info('[Lark ' . $module . '] Skipped in non-production.');
return self::SUCCESS;
}
$limit = (int) $this->option('limit');
$url = "https://hfbbr1an7s0.sg.larksuite.com/base/automation/webhook/event/{$webhookId}";
$statusM = ApprovalStatus::APPROVAL_STATUS_ID;
$bookings = Booking::query()
->whereNotExists(function ($q) {
$b = new Booking;
$q->selectRaw(1)
->from('lark_pushed')
->whereColumn('lark_pushed.owner_id', 'bookings.id')
->where('lark_pushed.owner_type', $b->getMorphClass())
->where('lark_pushed.is_success', 1);
})
->limit($limit)
->get();
if ($bookings->isEmpty()) {
$this->warn('[Lark ' . $module . '] Nothing to push.');
return self::SUCCESS;
}
$this->info("[Lark " . $module . "] Pushing {$bookings->count()} record(s)...");
Log::info('[Lark ' . $module . '] Start', ['limit' => $limit, 'env' => app()->environment()]);
foreach ($bookings as $b) {
$payload = [
'id' => (string) $b->id,
'marking' => (string) $b->marking,
'company_name' => (string) $b->company->name,
'company_reference' => (string) $b->company->reference,
'service_name' => (string) $b->service->name,
'currency' => (string) $b->fixedCurrency->short_code,
'amount' => $b->fix_amount,
'status' => (string) ($statusM[$b->status] ?? $b->status),
'created_at' => $b->created_at->format('Y-m-d H:i'),
'updated_at' => $b->updated_at->format('Y-m-d H:i'),
];
try {
$res = Http::asJson()->post($url, $payload);
$ok = $res->successful() && (int) data_get($res->json(), 'code', -1) === 0;
LarkPushed::create([
'owner_id' => $b->id,
'owner_type' => $b->getMorphClass(),
'is_success' => $ok ? 1 : 0,
]);
if ($ok) {
$this->info("[Lark $module]: ✅ Sent: {$b->id} | {$b->marking}");
} else {
$this->error("[Lark $module] Failed {$b->id} | {$b->marking}: {$res->status()} {$res->body()}");
Log::error('[Lark ' . $module . '] Push failed', [
'booking_id' => $b->id,
'status' => $res->status(),
'body' => $res->body(),
]);
}
} catch (\Throwable $e) {
LarkPushed::create([
'owner_id' => $b->id,
'owner_type' => $b->getMorphClass(),
'is_success' => 0,
]);
$this->error("[Lark $module]: ❌ Exception {$b->id} | {$b->marking}: {$e->getMessage()}");
Log::error('[Lark ' . $module . '] Exception', [
'booking_id' => $b->id,
'exception' => $e->getMessage(),
]);
}
sleep(1);
}
$this->info("[Lark " . $module . "] Finished {$bookings->count()} record(s).");
Log::info('[Lark ' . $module . '] Finished', ['count' => $bookings->count()]);
return self::SUCCESS;
}
}
@@ -27,7 +27,7 @@ class LarkPushCompanyModuleCommand extends Command
}
$this->info('Starting Lark push company module...');
Log::info('Lark push started', [
Log::info('[Company Module]: Lark push started', [
'limit' => (int) $this->option('limit'),
'env' => app()->environment(),
]);
@@ -49,7 +49,6 @@ class LarkPushCompanyModuleCommand extends Command
if ($companies->isEmpty()) {
$this->warn('⚠️ Nothing to push (all already marked success or none found).');
Log::info('Lark push: nothing to push');
return self::SUCCESS;
}
@@ -74,12 +73,7 @@ class LarkPushCompanyModuleCommand extends Command
]);
if ($ok) {
$this->info('✅ Sent: ' . $company->name . ' | ' . $company->reference);
Log::info('Lark push success', [
'company_id' => $company->id,
'name' => $company->name,
'reference' => $company->reference,
]);
$this->info('[Company Module]: ✅ Sent: ' . $company->name . ' | ' . $company->reference);
} else {
$this->error('❌ Failed for ' . $company->name . ': ' . $res->status() . ' ' . $res->body());
Log::error('Lark push failed', [
@@ -106,7 +100,7 @@ class LarkPushCompanyModuleCommand extends Command
}
$this->info('Finished sending ' . $companies->count() . ' records.');
Log::info('Lark push finished', ['count' => $companies->count()]);
Log::info('[Company Module]: Lark push finished', ['count' => $companies->count()]);
return self::SUCCESS;
}
+4
View File
@@ -75,6 +75,10 @@ class Kernel extends ConsoleKernel
$schedule->command('lark:push-company-module')
->hourly()
->withoutOverlapping();
// Push booking module to Lark
$schedule->command('lark:push-booking-module')
->hourly()
->withoutOverlapping();
}
}
//Commands Version 1: Before Laravel Vapor/AWS
+8 -3
View File
@@ -7,6 +7,7 @@ use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Models\Booking;
use App\Models\Transaction;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Log;
@@ -40,9 +41,13 @@ class TransactionResource extends JsonResource
$eInvoice = false;
if($booking && $this->type === TransactionType::REFUND){
$kvp = $this->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO_CREDIT_NOTE)->first();
if($kvp){
$eInvoice = true;
$kvp = $this->attributesKVP()->where('key', KVPKey::TRANSACTION_MODEL_CLASS)->first();
$transactionCreditNote = Transaction::where('id', $kvp->value)->first();
if($transactionCreditNote){
$kvp = $transactionCreditNote->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO_CREDIT_NOTE)->first();
if($kvp){
$eInvoice = true;
}
}
}
@@ -113,6 +113,9 @@
</modal-component>
</div>
</div>
<div class="row m-r-0 m-l-0 text-muted small">
<em>Note: Date filter not applicable to import</em>
</div>
<div class="row m-r-0 m-l-0" v-if="error">
<small class="bold fs-10 text-danger">{{error}}</small>
</div>
@@ -119,25 +119,27 @@
<td width="80%">
</td>
<td align="center" width="20%" style="float: right;">
<table width="100%">
<tbody>
<tr align="center">
<td>
<img src="{{ url(config('qr.qr_code_img_url') . $autocountEInvoiceValidationLink ) }}" style="width: 230px; height: 230px;" />
</td>
</tr>
<tr align="center">
<td>
<h2 style="margin: 0 !important;"><strong>{{ $autocountEInvoiceValidationLink }}</strong></h2>
</td>
</tr>
</tbody>
</table>
<td width="20%" valign="top" align="center">
<div style="display: inline-block; text-align: center; max-width: 230px; width: 100%;">
<img src="{{ url(config('qr.qr_code_img_url') . $autocountEInvoiceValidationLink ) }}"
style="width: 40%; height: auto; display: block;" />
<div style="margin-top: 5px;
word-break: break-word;
overflow-wrap: break-word;
white-space: normal;
font-size: 12px;
line-height: 1.2;
text-align: center;">
<strong>{{ $autocountEInvoiceValidationLink }}</strong>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table width="100%">
<tr>
<td style="text-align: right; ">This is generated by computer. No signature required.</td>