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"); } } } } }