Skip to content

Commit ed6ee87

Browse files
authored
Merge pull request #199 from fleetbase/dev-v1.6.38
v1.6.38
2 parents f6500fb + e88973d commit ed6ee87

17 files changed

Lines changed: 2368 additions & 63 deletions

composer.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fleetbase/core-api",
3-
"version": "1.6.37",
3+
"version": "1.6.38",
44
"description": "Core Framework and Resources for Fleetbase API",
55
"keywords": [
66
"fleetbase",
@@ -55,7 +55,9 @@
5555
"spatie/laravel-schedule-monitor": "^3.7",
5656
"spatie/laravel-sluggable": "^3.5",
5757
"sqids/sqids": "^0.4.1",
58-
"xantios/mimey": "^2.2.0"
58+
"xantios/mimey": "^2.2.0",
59+
"spatie/laravel-pdf": "^1.9",
60+
"mossadal/math-parser": "^1.3"
5961
},
6062
"require-dev": {
6163
"friendsofphp/php-cs-fixer": "^3.34.1",
@@ -105,4 +107,4 @@
105107
"@test:unit"
106108
]
107109
}
108-
}
110+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\DB;
6+
use Illuminate\Support\Facades\Schema;
7+
8+
/**
9+
* Improve the transactions table to serve as the platform-wide financial
10+
* transaction primitive.
11+
*
12+
* Changes:
13+
* - Rename owner_uuid/owner_type → subject_uuid/subject_type
14+
* - Add payer_uuid/payer_type (who the money flows from)
15+
* - Add payee_uuid/payee_type (who the money flows to)
16+
* - Add initiator_uuid/initiator_type (what triggered the transaction)
17+
* - Add context_uuid/context_type (related business object)
18+
* - Add direction (credit|debit)
19+
* - Add balance_after (running balance snapshot, wallet context)
20+
* - Add fee_amount, tax_amount, net_amount (monetary breakdown)
21+
* - Add exchange_rate, settled_currency, settled_amount (multi-currency)
22+
* - Add reference (idempotency key, unique)
23+
* - Add parent_transaction_uuid (refund/reversal/split linkage)
24+
* - Add gateway_response JSON (raw gateway payload)
25+
* - Add payment_method, payment_method_last4, payment_method_brand
26+
* - Add ip_address (fraud/audit trail)
27+
* - Add notes (internal operator annotation)
28+
* - Add failure_reason, failure_code (structured failure info)
29+
* - Add period (YYYY-MM accounting period, denormalised)
30+
* - Add tags JSON (operator-defined categorisation)
31+
* - Add settled_at, voided_at, reversed_at, expires_at timestamps
32+
* - Make amount NOT NULL DEFAULT 0
33+
* - Keep customer_uuid/customer_type as deprecated nullable aliases
34+
* - Keep owner_uuid/owner_type as deprecated nullable aliases (backfilled)
35+
*/
36+
return new class extends Migration {
37+
public function up(): void
38+
{
39+
Schema::table('transactions', function (Blueprint $table) {
40+
// ----------------------------------------------------------------
41+
// New polymorphic: subject (primary owner of the transaction record)
42+
// ----------------------------------------------------------------
43+
$table->char('subject_uuid', 36)->nullable()->after('owner_type');
44+
$table->string('subject_type')->nullable()->after('subject_uuid');
45+
46+
// ----------------------------------------------------------------
47+
// New polymorphic: payer (funds flow from)
48+
// ----------------------------------------------------------------
49+
$table->char('payer_uuid', 36)->nullable()->after('subject_type');
50+
$table->string('payer_type')->nullable()->after('payer_uuid');
51+
52+
// ----------------------------------------------------------------
53+
// New polymorphic: payee (funds flow to)
54+
// ----------------------------------------------------------------
55+
$table->char('payee_uuid', 36)->nullable()->after('payer_type');
56+
$table->string('payee_type')->nullable()->after('payee_uuid');
57+
58+
// ----------------------------------------------------------------
59+
// New polymorphic: initiator (what triggered the transaction)
60+
// ----------------------------------------------------------------
61+
$table->char('initiator_uuid', 36)->nullable()->after('payee_type');
62+
$table->string('initiator_type')->nullable()->after('initiator_uuid');
63+
64+
// ----------------------------------------------------------------
65+
// New polymorphic: context (related business object)
66+
// ----------------------------------------------------------------
67+
$table->char('context_uuid', 36)->nullable()->after('initiator_type');
68+
$table->string('context_type')->nullable()->after('context_uuid');
69+
70+
// ----------------------------------------------------------------
71+
// Direction and balance
72+
// ----------------------------------------------------------------
73+
$table->string('direction')->nullable()->after('status'); // credit | debit
74+
$table->integer('balance_after')->nullable()->after('direction');
75+
76+
// ----------------------------------------------------------------
77+
// Monetary breakdown (all in smallest currency unit / cents)
78+
// ----------------------------------------------------------------
79+
$table->integer('fee_amount')->default(0)->after('amount');
80+
$table->integer('tax_amount')->default(0)->after('fee_amount');
81+
$table->integer('net_amount')->default(0)->after('tax_amount');
82+
83+
// ----------------------------------------------------------------
84+
// Multi-currency settlement
85+
// ----------------------------------------------------------------
86+
$table->decimal('exchange_rate', 18, 8)->default(1)->after('currency');
87+
$table->string('settled_currency', 3)->nullable()->after('exchange_rate');
88+
$table->integer('settled_amount')->nullable()->after('settled_currency');
89+
90+
// ----------------------------------------------------------------
91+
// Idempotency and linkage
92+
// ----------------------------------------------------------------
93+
$table->string('reference', 191)->nullable()->unique()->after('description');
94+
$table->char('parent_transaction_uuid', 36)->nullable()->after('reference');
95+
96+
// ----------------------------------------------------------------
97+
// Gateway enrichment
98+
// ----------------------------------------------------------------
99+
$table->json('gateway_response')->nullable()->after('gateway_transaction_id');
100+
$table->string('payment_method', 50)->nullable()->after('gateway_response');
101+
$table->string('payment_method_last4', 4)->nullable()->after('payment_method');
102+
$table->string('payment_method_brand', 50)->nullable()->after('payment_method_last4');
103+
104+
// ----------------------------------------------------------------
105+
// Traceability and compliance
106+
// ----------------------------------------------------------------
107+
$table->string('ip_address', 45)->nullable()->after('meta');
108+
$table->text('notes')->nullable()->after('ip_address');
109+
$table->string('failure_reason', 191)->nullable()->after('notes');
110+
$table->string('failure_code', 50)->nullable()->after('failure_reason');
111+
112+
// ----------------------------------------------------------------
113+
// Reporting
114+
// ----------------------------------------------------------------
115+
$table->string('period', 7)->nullable()->after('failure_code'); // YYYY-MM
116+
$table->json('tags')->nullable()->after('period');
117+
118+
// ----------------------------------------------------------------
119+
// Lifecycle timestamps
120+
// ----------------------------------------------------------------
121+
$table->timestamp('settled_at')->nullable()->after('updated_at');
122+
$table->timestamp('voided_at')->nullable()->after('settled_at');
123+
$table->timestamp('reversed_at')->nullable()->after('voided_at');
124+
$table->timestamp('expires_at')->nullable()->after('reversed_at');
125+
});
126+
127+
// --------------------------------------------------------------------
128+
// Backfill subject_* from owner_* (preserve existing data)
129+
// --------------------------------------------------------------------
130+
DB::statement('UPDATE transactions SET subject_uuid = owner_uuid, subject_type = owner_type WHERE owner_uuid IS NOT NULL');
131+
132+
// --------------------------------------------------------------------
133+
// Backfill payer_* from customer_* (customer was semantically the payer)
134+
// --------------------------------------------------------------------
135+
DB::statement('UPDATE transactions SET payer_uuid = customer_uuid, payer_type = customer_type WHERE customer_uuid IS NOT NULL');
136+
137+
// --------------------------------------------------------------------
138+
// Backfill period from created_at
139+
// --------------------------------------------------------------------
140+
DB::statement("UPDATE transactions SET period = DATE_FORMAT(created_at, '%Y-%m') WHERE created_at IS NOT NULL");
141+
142+
// --------------------------------------------------------------------
143+
// Backfill net_amount = amount (no fees/tax in legacy records)
144+
// --------------------------------------------------------------------
145+
DB::statement('UPDATE transactions SET net_amount = COALESCE(amount, 0) WHERE net_amount = 0');
146+
147+
// --------------------------------------------------------------------
148+
// Add indexes on new columns
149+
// --------------------------------------------------------------------
150+
Schema::table('transactions', function (Blueprint $table) {
151+
$table->index(['subject_uuid', 'subject_type'], 'transactions_subject_index');
152+
$table->index(['payer_uuid', 'payer_type'], 'transactions_payer_index');
153+
$table->index(['payee_uuid', 'payee_type'], 'transactions_payee_index');
154+
$table->index(['initiator_uuid', 'initiator_type'], 'transactions_initiator_index');
155+
$table->index(['context_uuid', 'context_type'], 'transactions_context_index');
156+
$table->index('direction', 'transactions_direction_index');
157+
$table->index('period', 'transactions_period_index');
158+
$table->index('parent_transaction_uuid', 'transactions_parent_index');
159+
$table->index('payment_method', 'transactions_payment_method_index');
160+
$table->index('settled_at', 'transactions_settled_at_index');
161+
$table->index(['company_uuid', 'type'], 'transactions_company_type_index');
162+
$table->index(['company_uuid', 'status'], 'transactions_company_status_index');
163+
$table->index(['company_uuid', 'period'], 'transactions_company_period_index');
164+
});
165+
}
166+
167+
public function down(): void
168+
{
169+
Schema::table('transactions', function (Blueprint $table) {
170+
// Drop indexes
171+
$table->dropIndex('transactions_subject_index');
172+
$table->dropIndex('transactions_payer_index');
173+
$table->dropIndex('transactions_payee_index');
174+
$table->dropIndex('transactions_initiator_index');
175+
$table->dropIndex('transactions_context_index');
176+
$table->dropIndex('transactions_direction_index');
177+
$table->dropIndex('transactions_period_index');
178+
$table->dropIndex('transactions_parent_index');
179+
$table->dropIndex('transactions_payment_method_index');
180+
$table->dropIndex('transactions_settled_at_index');
181+
$table->dropIndex('transactions_company_type_index');
182+
$table->dropIndex('transactions_company_status_index');
183+
$table->dropIndex('transactions_company_period_index');
184+
$table->dropUnique(['reference']);
185+
186+
// Drop new columns
187+
$table->dropColumn([
188+
'subject_uuid', 'subject_type',
189+
'payer_uuid', 'payer_type',
190+
'payee_uuid', 'payee_type',
191+
'initiator_uuid', 'initiator_type',
192+
'context_uuid', 'context_type',
193+
'direction', 'balance_after',
194+
'fee_amount', 'tax_amount', 'net_amount',
195+
'exchange_rate', 'settled_currency', 'settled_amount',
196+
'reference', 'parent_transaction_uuid',
197+
'gateway_response',
198+
'payment_method', 'payment_method_last4', 'payment_method_brand',
199+
'ip_address', 'notes', 'failure_reason', 'failure_code',
200+
'period', 'tags',
201+
'settled_at', 'voided_at', 'reversed_at', 'expires_at',
202+
]);
203+
});
204+
}
205+
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\DB;
6+
use Illuminate\Support\Facades\Schema;
7+
8+
/**
9+
* Improve the transaction_items table to align with the robust transaction
10+
* primitive design.
11+
*
12+
* Changes:
13+
* - Add public_id (HasPublicId support)
14+
* - Fix amount column type: string → INT NOT NULL DEFAULT 0
15+
* - Add quantity (INT DEFAULT 1)
16+
* - Add unit_price (INT DEFAULT 0, cents)
17+
* - Add tax_rate (DECIMAL(5,2) DEFAULT 0.00)
18+
* - Add tax_amount (INT DEFAULT 0, cents)
19+
* - Add description (longer text alternative to details)
20+
* - Add sort_order (for ordered line item display)
21+
*/
22+
return new class extends Migration {
23+
public function up(): void
24+
{
25+
Schema::table('transaction_items', function (Blueprint $table) {
26+
// Add public_id for HasPublicId trait support
27+
$table->string('public_id', 191)->nullable()->unique()->after('uuid');
28+
29+
// Add quantity and unit price for proper line-item accounting
30+
$table->integer('quantity')->default(1)->after('transaction_uuid');
31+
$table->integer('unit_price')->default(0)->after('quantity');
32+
33+
// Add tax columns
34+
$table->decimal('tax_rate', 5, 2)->default(0.00)->after('currency');
35+
$table->integer('tax_amount')->default(0)->after('tax_rate');
36+
37+
// Add description as a longer alternative to details (TEXT vs VARCHAR)
38+
$table->text('description')->nullable()->after('details');
39+
40+
// Add sort order for ordered display of line items
41+
$table->unsignedSmallInteger('sort_order')->default(0)->after('description');
42+
});
43+
44+
// Fix amount column: string → integer
45+
// First copy to a temp column, then drop and re-add as integer
46+
DB::statement('ALTER TABLE transaction_items MODIFY COLUMN amount BIGINT NOT NULL DEFAULT 0');
47+
48+
// Backfill unit_price = amount for existing records (single-unit assumption)
49+
DB::statement('UPDATE transaction_items SET unit_price = amount WHERE unit_price = 0 AND amount > 0');
50+
}
51+
52+
public function down(): void
53+
{
54+
// Revert amount back to string (original type)
55+
DB::statement('ALTER TABLE transaction_items MODIFY COLUMN amount VARCHAR(191) NULL');
56+
57+
Schema::table('transaction_items', function (Blueprint $table) {
58+
$table->dropUnique(['public_id']);
59+
$table->dropColumn([
60+
'public_id',
61+
'quantity',
62+
'unit_price',
63+
'tax_rate',
64+
'tax_amount',
65+
'description',
66+
'sort_order',
67+
]);
68+
});
69+
}
70+
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration {
8+
/**
9+
* Run the migrations.
10+
*/
11+
public function up(): void
12+
{
13+
Schema::create('templates', function (Blueprint $table) {
14+
$table->bigIncrements('id');
15+
$table->string('uuid', 191)->unique()->nullable();
16+
$table->string('public_id', 191)->unique()->nullable();
17+
$table->string('company_uuid', 191)->nullable()->index();
18+
$table->string('created_by_uuid', 191)->nullable()->index();
19+
$table->string('updated_by_uuid', 191)->nullable()->index();
20+
21+
// Identity
22+
$table->string('name');
23+
$table->text('description')->nullable();
24+
25+
// Context type — defines which Fleetbase model this template is designed for.
26+
// e.g. 'order', 'invoice', 'transaction', 'shipping_label', 'receipt', 'report'
27+
$table->string('context_type')->default('generic')->index();
28+
29+
// Canvas dimensions (in mm by default, configurable via unit)
30+
$table->string('unit')->default('mm'); // mm, px, in
31+
$table->decimal('width', 10, 4)->default(210); // A4 width
32+
$table->decimal('height', 10, 4)->default(297); // A4 height
33+
$table->string('orientation')->default('portrait'); // portrait | landscape
34+
35+
// Page settings
36+
$table->json('margins')->nullable(); // { top, right, bottom, left }
37+
$table->string('background_color')->nullable();
38+
$table->string('background_image_uuid')->nullable();
39+
40+
// The full template content — array of element objects
41+
$table->longText('content')->nullable(); // JSON array of TemplateElement objects
42+
43+
// Element type definitions / schema overrides (optional per-template customisation)
44+
$table->json('element_schemas')->nullable();
45+
46+
// Status
47+
$table->boolean('is_default')->default(false);
48+
$table->boolean('is_system')->default(false);
49+
$table->boolean('is_public')->default(false);
50+
51+
$table->timestamps();
52+
$table->softDeletes();
53+
54+
// Foreign keys
55+
$table->foreign('company_uuid')->references('uuid')->on('companies')->onDelete('cascade');
56+
$table->foreign('created_by_uuid')->references('uuid')->on('users')->onDelete('set null');
57+
$table->foreign('updated_by_uuid')->references('uuid')->on('users')->onDelete('set null');
58+
59+
// Composite indexes
60+
$table->index(['company_uuid', 'context_type']);
61+
$table->index(['company_uuid', 'is_default']);
62+
});
63+
}
64+
65+
/**
66+
* Reverse the migrations.
67+
*/
68+
public function down(): void
69+
{
70+
Schema::dropIfExists('templates');
71+
}
72+
};

0 commit comments

Comments
 (0)