diff --git a/database/migrations/2026_07_17_200000_create_care_workflow_engine_tables.php b/database/migrations/2026_07_17_200000_create_care_workflow_engine_tables.php index 412bdef..cb3dea7 100644 --- a/database/migrations/2026_07_17_200000_create_care_workflow_engine_tables.php +++ b/database/migrations/2026_07_17_200000_create_care_workflow_engine_tables.php @@ -11,112 +11,123 @@ use Illuminate\Support\Facades\Schema; * VisitStageAdvance → a visit's position + history through those stages. * FinancialObligation → charge intent created before/after a stage; a stage's * queue stays gated until its obligation clears (paid / authorized / waived). + * + * Creation is idempotent so a prior partial deploy (tables present, migration + * row missing) can finish cleanly. */ return new class extends Migration { public function up(): void { - Schema::create('care_facility_workflows', function (Blueprint $table) { - $table->id(); - $table->uuid('uuid')->unique(); - $table->string('owner_ref')->index(); - $table->foreignId('organization_id')->constrained('care_organizations')->cascadeOnDelete(); - $table->foreignId('branch_id')->nullable()->constrained('care_branches')->nullOnDelete(); - $table->string('name'); - $table->string('template_key')->nullable(); - $table->string('category')->nullable(); - $table->boolean('is_active')->default(true); - $table->json('settings')->nullable(); - $table->timestamps(); - $table->softDeletes(); + if (! Schema::hasTable('care_facility_workflows')) { + Schema::create('care_facility_workflows', function (Blueprint $table) { + $table->id(); + $table->uuid('uuid')->unique(); + $table->string('owner_ref')->index(); + $table->foreignId('organization_id')->constrained('care_organizations')->cascadeOnDelete(); + $table->foreignId('branch_id')->nullable()->constrained('care_branches')->nullOnDelete(); + $table->string('name'); + $table->string('template_key')->nullable(); + $table->string('category')->nullable(); + $table->boolean('is_active')->default(true); + $table->json('settings')->nullable(); + $table->timestamps(); + $table->softDeletes(); - $table->index(['organization_id', 'branch_id', 'is_active']); - }); + $table->index(['organization_id', 'branch_id', 'is_active']); + }); + } - Schema::create('care_workflow_stages', function (Blueprint $table) { - $table->id(); - $table->uuid('uuid')->unique(); - $table->string('owner_ref')->index(); - $table->foreignId('workflow_id')->constrained('care_facility_workflows')->cascadeOnDelete(); - $table->string('code'); - $table->string('name'); - $table->string('type')->default('custom'); - $table->string('queue_context')->nullable(); - $table->string('department_type')->nullable(); - $table->unsignedInteger('sort_order')->default(0); + if (! Schema::hasTable('care_workflow_stages')) { + Schema::create('care_workflow_stages', function (Blueprint $table) { + $table->id(); + $table->uuid('uuid')->unique(); + $table->string('owner_ref')->index(); + $table->foreignId('workflow_id')->constrained('care_facility_workflows')->cascadeOnDelete(); + $table->string('code'); + $table->string('name'); + $table->string('type')->default('custom'); + $table->string('queue_context')->nullable(); + $table->string('department_type')->nullable(); + $table->unsignedInteger('sort_order')->default(0); - // Financial gate configuration - $table->boolean('requires_payment')->default(false); - $table->string('payment_timing')->default('none'); // before, after, none - $table->json('payment_modes')->nullable(); // internal_cashier, external_bank, digital - $table->boolean('insurance_eligible')->default(false); - $table->boolean('credit_allowed')->default(false); - $table->boolean('allow_override')->default(true); - $table->string('charge_code')->nullable(); - $table->string('charge_label')->nullable(); - $table->unsignedInteger('default_amount_minor')->nullable(); + // Financial gate configuration + $table->boolean('requires_payment')->default(false); + $table->string('payment_timing')->default('none'); // before, after, none + $table->json('payment_modes')->nullable(); // internal_cashier, external_bank, digital + $table->boolean('insurance_eligible')->default(false); + $table->boolean('credit_allowed')->default(false); + $table->boolean('allow_override')->default(true); + $table->string('charge_code')->nullable(); + $table->string('charge_label')->nullable(); + $table->unsignedInteger('default_amount_minor')->nullable(); - // Flow configuration - $table->string('default_next')->nullable(); - $table->boolean('can_return')->default(false); - $table->boolean('can_skip')->default(false); - $table->boolean('optional')->default(false); - $table->boolean('creates_child')->default(false); + // Flow configuration + $table->string('default_next')->nullable(); + $table->boolean('can_return')->default(false); + $table->boolean('can_skip')->default(false); + $table->boolean('optional')->default(false); + $table->boolean('creates_child')->default(false); - $table->json('meta')->nullable(); - $table->timestamps(); + $table->json('meta')->nullable(); + $table->timestamps(); - $table->unique(['workflow_id', 'code']); - $table->index(['workflow_id', 'sort_order']); - }); + $table->unique(['workflow_id', 'code']); + $table->index(['workflow_id', 'sort_order']); + }); + } - Schema::create('care_visit_stage_advances', function (Blueprint $table) { - $table->id(); - $table->string('owner_ref')->index(); - $table->foreignId('visit_id')->constrained('care_visits')->cascadeOnDelete(); - $table->foreignId('workflow_id')->nullable()->constrained('care_facility_workflows')->nullOnDelete(); - $table->foreignId('workflow_stage_id')->nullable()->constrained('care_workflow_stages')->nullOnDelete(); - $table->string('stage_code')->nullable(); - $table->string('status')->default('active'); // active, blocked, completed, skipped - $table->string('blocked_reason')->nullable(); // awaiting_payment, awaiting_bank_receipt, awaiting_authorization - $table->timestamp('entered_at')->nullable(); - $table->timestamp('cleared_at')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->json('meta')->nullable(); - $table->timestamps(); + if (! Schema::hasTable('care_visit_stage_advances')) { + Schema::create('care_visit_stage_advances', function (Blueprint $table) { + $table->id(); + $table->string('owner_ref')->index(); + $table->foreignId('visit_id')->constrained('care_visits')->cascadeOnDelete(); + $table->foreignId('workflow_id')->nullable()->constrained('care_facility_workflows')->nullOnDelete(); + $table->foreignId('workflow_stage_id')->nullable()->constrained('care_workflow_stages')->nullOnDelete(); + $table->string('stage_code')->nullable(); + $table->string('status')->default('active'); // active, blocked, completed, skipped + $table->string('blocked_reason')->nullable(); // awaiting_payment, awaiting_bank_receipt, awaiting_authorization + $table->timestamp('entered_at')->nullable(); + $table->timestamp('cleared_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->json('meta')->nullable(); + $table->timestamps(); - $table->index(['visit_id', 'status']); - $table->index(['visit_id', 'stage_code']); - }); + $table->index(['visit_id', 'status']); + $table->index(['visit_id', 'stage_code']); + }); + } - Schema::create('care_financial_obligations', function (Blueprint $table) { - $table->id(); - $table->uuid('uuid')->unique(); - $table->string('owner_ref')->index(); - $table->foreignId('organization_id')->constrained('care_organizations')->cascadeOnDelete(); - $table->foreignId('branch_id')->nullable()->constrained('care_branches')->nullOnDelete(); - $table->foreignId('visit_id')->nullable()->constrained('care_visits')->nullOnDelete(); - $table->foreignId('patient_id')->nullable()->constrained('care_patients')->nullOnDelete(); - $table->foreignId('workflow_stage_id')->nullable()->constrained('care_workflow_stages')->nullOnDelete(); - $table->foreignId('bill_id')->nullable()->constrained('care_bills')->nullOnDelete(); - $table->string('stage_code')->nullable(); - $table->string('charge_code')->nullable(); - $table->string('label')->nullable(); - $table->unsignedInteger('amount_minor')->default(0); - $table->string('status')->default('pending'); // pending, authorized, paid, waived, deferred, void - $table->string('clearance_method')->nullable(); // payment, insurance, waiver, credit, bank_receipt, override - $table->string('payment_mode')->nullable(); // internal_cashier, external_bank, digital - $table->string('external_reference')->nullable(); // bank slip / receipt no - $table->string('cleared_by')->nullable(); - $table->timestamp('cleared_at')->nullable(); - $table->json('meta')->nullable(); - $table->timestamps(); - $table->softDeletes(); + if (! Schema::hasTable('care_financial_obligations')) { + Schema::create('care_financial_obligations', function (Blueprint $table) { + $table->id(); + $table->uuid('uuid')->unique(); + $table->string('owner_ref')->index(); + $table->foreignId('organization_id')->constrained('care_organizations')->cascadeOnDelete(); + $table->foreignId('branch_id')->nullable()->constrained('care_branches')->nullOnDelete(); + $table->foreignId('visit_id')->nullable()->constrained('care_visits')->nullOnDelete(); + $table->foreignId('patient_id')->nullable()->constrained('care_patients')->nullOnDelete(); + $table->foreignId('workflow_stage_id')->nullable()->constrained('care_workflow_stages')->nullOnDelete(); + $table->foreignId('bill_id')->nullable()->constrained('care_bills')->nullOnDelete(); + $table->string('stage_code')->nullable(); + $table->string('charge_code')->nullable(); + $table->string('label')->nullable(); + $table->unsignedInteger('amount_minor')->default(0); + $table->string('status')->default('pending'); // pending, authorized, paid, waived, deferred, void + $table->string('clearance_method')->nullable(); // payment, insurance, waiver, credit, bank_receipt, override + $table->string('payment_mode')->nullable(); // internal_cashier, external_bank, digital + $table->string('external_reference')->nullable(); // bank slip / receipt no + $table->string('cleared_by')->nullable(); + $table->timestamp('cleared_at')->nullable(); + $table->json('meta')->nullable(); + $table->timestamps(); + $table->softDeletes(); - $table->index(['visit_id', 'status']); - $table->index(['organization_id', 'status']); - $table->index(['stage_code', 'status']); - }); + $table->index(['visit_id', 'status']); + $table->index(['organization_id', 'status']); + $table->index(['stage_code', 'status']); + }); + } } public function down(): void diff --git a/database/migrations/2026_07_17_210000_enforce_unique_care_workflow_obligations.php b/database/migrations/2026_07_17_210000_enforce_unique_care_workflow_obligations.php index 1e07b5a..aa62cd8 100644 --- a/database/migrations/2026_07_17_210000_enforce_unique_care_workflow_obligations.php +++ b/database/migrations/2026_07_17_210000_enforce_unique_care_workflow_obligations.php @@ -8,6 +8,17 @@ return new class extends Migration { public function up(): void { + if (! Schema::hasTable('care_financial_obligations')) { + return; + } + + $hasUnique = collect(Schema::getIndexes('care_financial_obligations')) + ->contains(fn (array $index) => ($index['name'] ?? '') === 'care_obligations_visit_stage_unique'); + + if ($hasUnique) { + return; + } + Schema::table('care_financial_obligations', function (Blueprint $table) { $table->unique( ['visit_id', 'workflow_stage_id'], @@ -18,6 +29,17 @@ return new class extends Migration public function down(): void { + if (! Schema::hasTable('care_financial_obligations')) { + return; + } + + $hasUnique = collect(Schema::getIndexes('care_financial_obligations')) + ->contains(fn (array $index) => ($index['name'] ?? '') === 'care_obligations_visit_stage_unique'); + + if (! $hasUnique) { + return; + } + Schema::table('care_financial_obligations', function (Blueprint $table) { $table->dropUnique('care_obligations_visit_stage_unique'); });