Skip to content

Commit 8f317da

Browse files
committed
feat(phase-8): add cross-module integration events and listeners
Six integration workflows connecting isolated ERP modules: - Purchase PO confirmed → Inventory GoodsReceipt auto-created - CRM Deal won → Finance draft Invoice auto-created - Inventory stock low → Purchase RFQ auto-created (with reorder rule trigger) - HR Payroll approved → Accounting JournalEntry + lines auto-created - Subscription renewed → Finance draft Invoice auto-created - Manufacturing Order completed → Inventory StockMovement (finished goods) created Trigger methods added to models: Po::confirm(), CrmLead::markWon(), StockLevel::checkReorderRules(), PayrollRun::approve(), Subscription::renew(), ManufacturingOrder::complete(). Uses Laravel auto-discovery (withEvents() from ApplicationBuilder) — no duplicate registration via explicit EventServiceProvider. Also makes goods_receipt_items.product_id nullable to support PO lines that don't yet have a matching catalog product. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3fd4f2a commit 8f317da

20 files changed

Lines changed: 544 additions & 6 deletions

File tree

erp/app/Events/CRM/CrmDealWon.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\Events\CRM;
4+
5+
use App\Modules\CRM\Models\CrmLead;
6+
7+
class CrmDealWon
8+
{
9+
public function __construct(public readonly CrmLead $lead) {}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\Events\HR;
4+
5+
use App\Modules\HR\Models\PayrollRun;
6+
7+
class PayrollRunApproved
8+
{
9+
public function __construct(public readonly PayrollRun $payrollRun) {}
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace App\Events\Inventory;
4+
5+
use App\Modules\Inventory\Models\Product;
6+
use App\Modules\Inventory\Models\StockLevel;
7+
use App\Modules\Inventory\Models\ReorderRule;
8+
9+
class InventoryStockLow
10+
{
11+
public function __construct(
12+
public readonly Product $product,
13+
public readonly StockLevel $stockLevel,
14+
public readonly ReorderRule $reorderRule,
15+
) {}
16+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\Events\Manufacturing;
4+
5+
use App\Modules\Manufacturing\Models\ManufacturingOrder;
6+
7+
class ManufacturingOrderCompleted
8+
{
9+
public function __construct(public readonly ManufacturingOrder $order) {}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\Events\Purchase;
4+
5+
use App\Modules\Purchase\Models\Po;
6+
7+
class PurchaseOrderConfirmed
8+
{
9+
public function __construct(public readonly Po $po) {}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\Events\Subscriptions;
4+
5+
use App\Modules\Subscriptions\Models\Subscription;
6+
7+
class SubscriptionRenewed
8+
{
9+
public function __construct(public readonly Subscription $subscription) {}
10+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace App\Listeners\CRM;
4+
5+
use App\Events\CRM\CrmDealWon;
6+
use App\Modules\Finance\Models\Invoice;
7+
use App\Modules\Finance\Models\InvoiceItem;
8+
9+
class CreateFinanceInvoiceFromDeal
10+
{
11+
public function handle(CrmDealWon $event): void
12+
{
13+
$lead = $event->lead;
14+
15+
$invoice = Invoice::create([
16+
'tenant_id' => $lead->tenant_id,
17+
'number' => 'INV-CRM-' . $lead->id . '-' . uniqid(),
18+
'issue_date' => now()->toDateString(),
19+
'due_date' => now()->addDays(30)->toDateString(),
20+
'status' => 'draft',
21+
'notes' => 'Auto-created from CRM deal: ' . $lead->title,
22+
]);
23+
24+
InvoiceItem::create([
25+
'invoice_id' => $invoice->id,
26+
'description' => $lead->title,
27+
'quantity' => 1,
28+
'unit_price' => $lead->expected_revenue ?? 0,
29+
'tax_rate' => 0,
30+
]);
31+
}
32+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace App\Listeners\HR;
4+
5+
use App\Events\HR\PayrollRunApproved;
6+
use App\Modules\Accounting\Models\JournalEntry;
7+
use App\Modules\Accounting\Models\JournalEntryLine;
8+
9+
class CreatePayrollJournalEntry
10+
{
11+
public function handle(PayrollRunApproved $event): void
12+
{
13+
$payrollRun = $event->payrollRun;
14+
15+
$entry = JournalEntry::create([
16+
'tenant_id' => $payrollRun->tenant_id,
17+
'reference' => 'PAYROLL-' . $payrollRun->period_label,
18+
'description' => 'Payroll journal entry for ' . $payrollRun->period_label,
19+
'entry_date' => now()->toDateString(),
20+
'status' => 'draft',
21+
]);
22+
23+
$entry->entry_number = $entry->generateEntryNumber();
24+
$entry->save();
25+
26+
// Find or create placeholder accounts for salary expense and payable
27+
$expenseAccount = $this->findOrCreateAccount($payrollRun->tenant_id, '6100', 'Salary Expense', 'expense', 'debit');
28+
$payableAccount = $this->findOrCreateAccount($payrollRun->tenant_id, '2100', 'Salary Payable', 'liability', 'credit');
29+
30+
JournalEntryLine::create([
31+
'journal_entry_id' => $entry->id,
32+
'account_id' => $expenseAccount->id,
33+
'description' => 'Salary Expense',
34+
'debit' => $payrollRun->total_gross,
35+
'credit' => 0,
36+
]);
37+
38+
JournalEntryLine::create([
39+
'journal_entry_id' => $entry->id,
40+
'account_id' => $payableAccount->id,
41+
'description' => 'Salary Payable',
42+
'debit' => 0,
43+
'credit' => $payrollRun->total_net,
44+
]);
45+
}
46+
47+
private function findOrCreateAccount(int $tenantId, string $code, string $name, string $type, string $normalBalance): \App\Modules\Accounting\Models\Account
48+
{
49+
return \App\Modules\Accounting\Models\Account::firstOrCreate(
50+
['tenant_id' => $tenantId, 'code' => $code],
51+
[
52+
'name' => $name,
53+
'type' => $type,
54+
'normal_balance' => $normalBalance,
55+
'is_active' => true,
56+
]
57+
);
58+
}
59+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace App\Listeners\Inventory;
4+
5+
use App\Events\Inventory\InventoryStockLow;
6+
use App\Modules\Purchase\Models\PurchaseRfq;
7+
use App\Modules\Purchase\Models\PurchaseRfqLine;
8+
9+
class CreatePurchaseRfqFromLowStock
10+
{
11+
public function handle(InventoryStockLow $event): void
12+
{
13+
$product = $event->product;
14+
$reorderRule = $event->reorderRule;
15+
16+
$vendor = \App\Modules\Purchase\Models\PurchaseVendor::where('tenant_id', $product->tenant_id)
17+
->where('is_active', true)
18+
->first();
19+
20+
if (! $vendor) {
21+
$vendor = \App\Modules\Purchase\Models\PurchaseVendor::create([
22+
'tenant_id' => $product->tenant_id,
23+
'name' => 'Default Vendor',
24+
'currency' => 'USD',
25+
'is_active' => true,
26+
]);
27+
}
28+
29+
$rfq = PurchaseRfq::create([
30+
'tenant_id' => $product->tenant_id,
31+
'rfq_number' => 'RFQ-AUTO-' . now()->format('YmdHis') . '-' . uniqid(),
32+
'po_vendor_id' => $vendor->id,
33+
'status' => 'draft',
34+
'currency' => 'USD',
35+
'notes' => 'Auto-created from low stock alert for: ' . $product->name,
36+
]);
37+
38+
PurchaseRfqLine::create([
39+
'tenant_id' => $product->tenant_id,
40+
'po_rfq_id' => $rfq->id,
41+
'product_name' => $product->name,
42+
'quantity' => $reorderRule->reorder_quantity,
43+
'unit_price' => 0,
44+
'subtotal' => 0,
45+
]);
46+
47+
$reorderRule->trigger();
48+
}
49+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace App\Listeners\Manufacturing;
4+
5+
use App\Events\Manufacturing\ManufacturingOrderCompleted;
6+
use App\Modules\Inventory\Models\StockMovement;
7+
8+
class UpdateInventoryForManufacturingOrder
9+
{
10+
public function handle(ManufacturingOrderCompleted $event): void
11+
{
12+
$order = $event->order;
13+
14+
StockMovement::create([
15+
'tenant_id' => $order->tenant_id,
16+
'product_id' => $order->product_id,
17+
'warehouse_id' => $order->warehouse_id,
18+
'type' => 'in',
19+
'quantity' => $order->qty_produced,
20+
'reference' => 'MO-COMPLETION-' . $order->mo_number,
21+
'notes' => 'Finished goods from manufacturing order ' . $order->mo_number,
22+
]);
23+
}
24+
}

0 commit comments

Comments
 (0)