@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Tempo\Finance\Payables;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/** Shared additive schema; installation never grants access or imports documents. */
|
||||
final class PayablesSchema
|
||||
{
|
||||
public static function install(): void
|
||||
{
|
||||
$driver = DB::connection()->getDriverName();
|
||||
if (! in_array($driver, ['pgsql', 'sqlite'], true)) {
|
||||
throw new \LogicException('Unsupported payables database.');
|
||||
}
|
||||
$tables = ['pa_sources', 'pa_proposals', 'pa_obligations', 'pa_events', 'pa_outbox', 'pa_commands'];
|
||||
$all = [...$tables, 'pa_access'];
|
||||
$existing = array_filter($all, fn ($table) => Schema::hasTable($table));
|
||||
if ($existing) {
|
||||
throw new \LogicException('Payables tables already exist; explicit schema reconciliation required.');
|
||||
}
|
||||
$scope = function (Blueprint $t) {
|
||||
foreach (['tenant_id', 'company_id', 'unit_id'] as $k) {
|
||||
$t->unsignedBigInteger($k);
|
||||
}
|
||||
};
|
||||
Schema::create('pa_access', function (Blueprint $t) use ($scope) {
|
||||
$t->id();
|
||||
$scope($t);
|
||||
$t->unsignedBigInteger('user_id');
|
||||
$t->string('person');
|
||||
$t->string('action');
|
||||
$t->unique(['tenant_id', 'company_id', 'unit_id', 'user_id', 'action']);
|
||||
});
|
||||
Schema::create('pa_sources', function (Blueprint $t) use ($scope) {
|
||||
$t->id();
|
||||
$scope($t);
|
||||
$t->string('source_key', 64)->unique();
|
||||
$t->unsignedBigInteger('local_source_id');
|
||||
$t->timestamp('created_at');
|
||||
});
|
||||
Schema::create('pa_proposals', function (Blueprint $t) use ($scope) {
|
||||
$t->id();
|
||||
$scope($t);
|
||||
$t->foreignId('source_id')->constrained('pa_sources');
|
||||
$t->string('source_revision', 64);
|
||||
$t->integer('capture_version');
|
||||
$t->jsonb('snapshot');
|
||||
$t->jsonb('missing');
|
||||
$t->unsignedBigInteger('prepared_by');
|
||||
$t->string('prepared_person');
|
||||
$t->text('reason');
|
||||
$t->timestamp('created_at');
|
||||
$t->unique(['source_id', 'capture_version']);
|
||||
});
|
||||
Schema::create('pa_obligations', function (Blueprint $t) use ($scope) {
|
||||
$t->id();
|
||||
$scope($t);
|
||||
$t->foreignId('source_id')->unique()->constrained('pa_sources');
|
||||
$t->foreignId('proposal_id')->constrained('pa_proposals');
|
||||
$t->string('source_revision', 64);
|
||||
$t->bigInteger('principal_minor');
|
||||
$t->string('currency', 3);
|
||||
$t->date('due_on')->nullable();
|
||||
$t->unsignedBigInteger('recognised_by');
|
||||
$t->string('recognised_person');
|
||||
$t->text('reason');
|
||||
$t->timestamp('created_at');
|
||||
});
|
||||
Schema::create('pa_commands', function (Blueprint $t) use ($scope) {
|
||||
$t->id();
|
||||
$scope($t);
|
||||
$t->unsignedBigInteger('actor_id');
|
||||
$t->uuid('command_key');
|
||||
$t->string('request_hash', 64);
|
||||
$t->jsonb('result');
|
||||
$t->timestamp('created_at');
|
||||
$t->unique(['tenant_id', 'company_id', 'unit_id', 'actor_id', 'command_key']);
|
||||
});
|
||||
Schema::create('pa_events', function (Blueprint $t) use ($scope) {
|
||||
$t->id();
|
||||
$scope($t);
|
||||
$t->foreignId('source_id')->constrained('pa_sources');
|
||||
$t->unsignedBigInteger('actor_id');
|
||||
$t->string('kind');
|
||||
$t->jsonb('payload');
|
||||
$t->timestamp('created_at');
|
||||
});
|
||||
Schema::create('pa_outbox', function (Blueprint $t) {
|
||||
$t->foreignId('event_id')->primary()->constrained('pa_events');
|
||||
$t->string('state');
|
||||
});
|
||||
if ($driver === 'pgsql') {
|
||||
DB::unprepared("CREATE FUNCTION pa_append_only() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'PA_APPEND_ONLY'; END $$");
|
||||
foreach ($tables as $table) {
|
||||
DB::unprepared("CREATE TRIGGER pa_history_immutable BEFORE UPDATE OR DELETE ON {$table} FOR EACH ROW EXECUTE FUNCTION pa_append_only()");
|
||||
}
|
||||
} else {
|
||||
foreach ($tables as $table) {
|
||||
foreach (['UPDATE', 'DELETE'] as $operation) {
|
||||
$trigger = $table.'_immutable_'.strtolower($operation);
|
||||
DB::unprepared("CREATE TRIGGER {$trigger} BEFORE {$operation} ON {$table} BEGIN SELECT RAISE(ABORT, 'PA_APPEND_ONLY'); END");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user