diff --git a/app/Models/CollectionLog.php b/app/Models/CollectionLog.php index a213f55..52eba98 100644 --- a/app/Models/CollectionLog.php +++ b/app/Models/CollectionLog.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -10,7 +11,7 @@ use MatanYadaev\EloquentSpatial\Traits\HasSpatial; class CollectionLog extends Model { - use HasFactory, HasSpatial; + use HasFactory, HasSpatial, HasTenant; public const STATUS_VALID = 'valid'; public const STATUS_INVALID = 'invalid'; @@ -18,7 +19,7 @@ class CollectionLog extends Model public const STATUS_EXPIRED = 'expired'; protected $fillable = [ - 'qr_code_id', 'household_id', 'drop_off_point_id', + 'tenant_id', 'qr_code_id', 'household_id', 'drop_off_point_id', 'scanned_by_user_id', 'trip_id', 'trip_stop_id', 'scanned_at', 'coordinates_at_scan', 'weight_kg', 'waste_type', 'photo_path', 'notes', diff --git a/app/Models/CollectionTeam.php b/app/Models/CollectionTeam.php index e9758d3..59728ba 100644 --- a/app/Models/CollectionTeam.php +++ b/app/Models/CollectionTeam.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -11,13 +12,13 @@ use Illuminate\Support\Str; class CollectionTeam extends Model { - use HasFactory, SoftDeletes; + use HasFactory, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; public const STATUS_INACTIVE = 'inactive'; protected $fillable = [ - 'uuid', 'name', 'area_id', 'driver_id', 'scanner_id', 'truck_id', + 'uuid', 'tenant_id', 'name', 'area_id', 'driver_id', 'scanner_id', 'truck_id', 'status', 'notes', ]; diff --git a/app/Models/DailyCollectionStat.php b/app/Models/DailyCollectionStat.php index ce09764..eca67b3 100644 --- a/app/Models/DailyCollectionStat.php +++ b/app/Models/DailyCollectionStat.php @@ -2,16 +2,17 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class DailyCollectionStat extends Model { - use HasFactory; + use HasFactory, HasTenant; protected $fillable = [ - 'date', 'barangay_id', 'total_scans', 'total_weight_kg', + 'tenant_id', 'date', 'barangay_id', 'total_scans', 'total_weight_kg', 'unique_households', 'missed_pickups', ]; diff --git a/app/Models/DumpsiteRelease.php b/app/Models/DumpsiteRelease.php index 3569721..206f371 100644 --- a/app/Models/DumpsiteRelease.php +++ b/app/Models/DumpsiteRelease.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -14,9 +15,10 @@ use MatanYadaev\EloquentSpatial\Traits\HasSpatial; */ class DumpsiteRelease extends Model { - use HasFactory, HasSpatial; + use HasFactory, HasSpatial, HasTenant; protected $fillable = [ + 'tenant_id', 'trip_id', 'dumpsite_id', 'released_at', diff --git a/app/Models/MonthlyStoreSale.php b/app/Models/MonthlyStoreSale.php index 1a0ca70..07c24e1 100644 --- a/app/Models/MonthlyStoreSale.php +++ b/app/Models/MonthlyStoreSale.php @@ -2,16 +2,17 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class MonthlyStoreSale extends Model { - use HasFactory; + use HasFactory, HasTenant; protected $fillable = [ - 'month_start_date', 'store_id', + 'tenant_id', 'month_start_date', 'store_id', 'total_quantity_sold', 'total_retail_centavos', 'total_commission_centavos', ]; diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 6286419..cf823cf 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -11,7 +12,7 @@ use Spatie\Activitylog\Traits\LogsActivity; class Payment extends Model { - use HasFactory, LogsActivity; + use HasFactory, HasTenant, LogsActivity; public const PURPOSE_RESIDENT = 'resident_code_purchase'; public const PURPOSE_STORE_WHOLESALE = 'store_inventory_purchase'; @@ -26,7 +27,7 @@ class Payment extends Model public const PROVIDER_MANUAL = 'manual'; protected $fillable = [ - 'uuid', 'payer_user_id', 'purpose', 'amount_centavos', 'currency', + 'uuid', 'tenant_id', 'payer_user_id', 'purpose', 'amount_centavos', 'currency', 'provider', 'provider_payment_id', 'status', 'provider_data', 'metadata', 'paid_at', ]; diff --git a/app/Models/QrCode.php b/app/Models/QrCode.php index 6acde87..af74149 100644 --- a/app/Models/QrCode.php +++ b/app/Models/QrCode.php @@ -3,6 +3,7 @@ namespace App\Models; use App\States\QrCode\QrCodeState; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -12,9 +13,10 @@ use Spatie\ModelStates\HasStates; class QrCode extends Model { - use HasFactory, HasStates, LogsActivity; + use HasFactory, HasStates, HasTenant, LogsActivity; protected $fillable = [ + 'tenant_id', 'serial', 'barcode_value', 'batch_id', diff --git a/app/Models/QrCodeBatch.php b/app/Models/QrCodeBatch.php index d4ab8ce..cb6c312 100644 --- a/app/Models/QrCodeBatch.php +++ b/app/Models/QrCodeBatch.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; class QrCodeBatch extends Model { - use HasFactory, SoftDeletes; + use HasFactory, HasTenant, SoftDeletes; public const PURPOSE_FREE = 'free_allocation'; public const PURPOSE_STORE = 'store_inventory'; @@ -19,6 +20,7 @@ class QrCodeBatch extends Model protected $table = 'qr_code_batches'; protected $fillable = [ + 'tenant_id', 'batch_number', 'quantity', 'purpose', diff --git a/app/Models/Route.php b/app/Models/Route.php index 73705b8..9315dc1 100644 --- a/app/Models/Route.php +++ b/app/Models/Route.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -11,13 +12,14 @@ use Illuminate\Support\Str; class Route extends Model { - use HasFactory, SoftDeletes; + use HasFactory, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; public const STATUS_INACTIVE = 'inactive'; protected $fillable = [ 'uuid', + 'tenant_id', 'name', 'code', 'area_id', diff --git a/app/Models/ServiceArea.php b/app/Models/ServiceArea.php index 56504a5..117503a 100644 --- a/app/Models/ServiceArea.php +++ b/app/Models/ServiceArea.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -10,13 +11,14 @@ use Illuminate\Support\Str; class ServiceArea extends Model { - use HasFactory, SoftDeletes; + use HasFactory, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; public const STATUS_INACTIVE = 'inactive'; protected $fillable = [ 'uuid', + 'tenant_id', 'name', 'code', 'status', diff --git a/app/Models/Trip.php b/app/Models/Trip.php index 034d586..ca2fc96 100644 --- a/app/Models/Trip.php +++ b/app/Models/Trip.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -13,7 +14,7 @@ use Spatie\Activitylog\Traits\LogsActivity; class Trip extends Model { - use HasFactory, LogsActivity, SoftDeletes; + use HasFactory, HasTenant, LogsActivity, SoftDeletes; public const STATUS_SCHEDULED = 'scheduled'; public const STATUS_IN_PROGRESS = 'in_progress'; @@ -22,7 +23,7 @@ class Trip extends Model public const STATUS_CANCELLED = 'cancelled'; protected $fillable = [ - 'uuid', 'trip_number', 'route_id', 'team_id', 'truck_id', 'dumpsite_id', + 'uuid', 'tenant_id', 'trip_number', 'route_id', 'team_id', 'truck_id', 'dumpsite_id', 'scheduled_date', 'scheduled_start_time', 'actual_start_time', 'actual_end_time', 'dumpsite_arrival_time', 'dumpsite_departure_time', diff --git a/app/Models/Truck.php b/app/Models/Truck.php index c3c5c97..1e882fa 100644 --- a/app/Models/Truck.php +++ b/app/Models/Truck.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -12,14 +13,14 @@ use MatanYadaev\EloquentSpatial\Traits\HasSpatial; class Truck extends Model { - use HasFactory, HasSpatial, SoftDeletes; + use HasFactory, HasSpatial, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; public const STATUS_MAINTENANCE = 'maintenance'; public const STATUS_RETIRED = 'retired'; protected $fillable = [ - 'uuid', 'plate_number', 'model', 'capacity_kg', + 'uuid', 'tenant_id', 'plate_number', 'model', 'capacity_kg', 'assigned_team_id', 'last_known_coordinates', 'last_location_updated_at', 'status', 'notes', ]; diff --git a/app/Models/WeeklyRoutePerformance.php b/app/Models/WeeklyRoutePerformance.php index c849043..520b0ea 100644 --- a/app/Models/WeeklyRoutePerformance.php +++ b/app/Models/WeeklyRoutePerformance.php @@ -2,18 +2,19 @@ namespace App\Models; +use App\Tenancy\HasTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class WeeklyRoutePerformance extends Model { - use HasFactory; + use HasFactory, HasTenant; protected $table = 'weekly_route_performance'; protected $fillable = [ - 'week_start_date', 'route_id', + 'tenant_id', 'week_start_date', 'route_id', 'on_time_rate_percent', 'avg_trip_duration_minutes', 'completion_rate_percent', 'trips_count', ]; diff --git a/app/Services/Qr/BatchGenerator.php b/app/Services/Qr/BatchGenerator.php index bcbca33..999901e 100644 --- a/app/Services/Qr/BatchGenerator.php +++ b/app/Services/Qr/BatchGenerator.php @@ -7,6 +7,7 @@ use App\Models\QrCodeBatch; use App\Models\ServiceArea; use App\Models\User; use App\States\QrCode\Unassigned; +use App\Tenancy\Tenancy; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; @@ -48,9 +49,15 @@ class BatchGenerator $rows = []; $tsNow = $now->toDateTimeString(); + // Bulk insert bypasses model events, so we have to set tenant_id + // explicitly. Inherit from the just-created batch (which goes + // through the HasTenant trait) so any cross-tenant subtleties + // bubble up early. + $tenantId = $batch->tenant_id ?? Tenancy::current()?->id; for ($i = 1; $i <= $quantity; $i++) { $serial = $this->serials->build($areaCode, $now, $batchSeq, $i); $rows[] = [ + 'tenant_id' => $tenantId, 'serial' => $serial, 'barcode_value' => $serial, 'batch_id' => $batch->id, diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 0cd663f..eb61aa3 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -25,6 +25,7 @@ class UserFactory extends Factory { return [ 'uuid' => (string) Str::uuid(), + 'tenant_id' => \App\Tenancy\Tenancy::current()?->id, 'first_name' => fake()->firstName(), 'last_name' => fake()->lastName(), 'email' => fake()->unique()->safeEmail(), diff --git a/database/migrations/2026_06_01_100003_add_tenant_id_to_phase_b_tables.php b/database/migrations/2026_06_01_100003_add_tenant_id_to_phase_b_tables.php new file mode 100644 index 0000000..8a8ff3b --- /dev/null +++ b/database/migrations/2026_06_01_100003_add_tenant_id_to_phase_b_tables.php @@ -0,0 +1,63 @@ +foreignId('tenant_id') + ->nullable() + ->after('id') + ->constrained('tenants') + ->nullOnDelete(); + $t->index(['tenant_id'], "{$table}_tenant_id_idx"); + }); + } + } + } + + public function down(): void + { + foreach (self::TABLES as $table) { + if (Schema::hasColumn($table, 'tenant_id')) { + Schema::table($table, function (Blueprint $t) use ($table) { + $t->dropIndex("{$table}_tenant_id_idx"); + $t->dropConstrainedForeignId('tenant_id'); + }); + } + } + } +}; diff --git a/database/seeders/SanPascualTenantSeeder.php b/database/seeders/SanPascualTenantSeeder.php index 073266e..83b8703 100644 --- a/database/seeders/SanPascualTenantSeeder.php +++ b/database/seeders/SanPascualTenantSeeder.php @@ -89,10 +89,19 @@ class SanPascualTenantSeeder extends Seeder */ private function backfillExistingRows(int $tenantId): void { - $tables = ['users', 'households', 'drop_off_points', 'dumpsites', 'partner_stores']; + // Tables with their own tenant_id column (HasTenant applied). + $tables = [ + 'users', 'households', 'drop_off_points', 'dumpsites', 'partner_stores', + 'service_areas', 'routes', 'collection_teams', 'trucks', 'trips', + 'qr_code_batches', 'qr_codes', 'collection_logs', 'dumpsite_releases', + 'payments', + 'daily_collection_stats', 'weekly_route_performance', 'monthly_store_sales', + ]; $totals = []; foreach ($tables as $table) { + if (! \Illuminate\Support\Facades\Schema::hasColumn($table, 'tenant_id')) continue; + // Don't backfill super_admins — they have no tenant. if ($table === 'users') { $count = DB::table('users') diff --git a/tests/TestCase.php b/tests/TestCase.php index fe1ffc2..6c1c222 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,9 +2,38 @@ namespace Tests; +use App\Models\Tenant; +use App\Tenancy\Tenancy; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { - // + /** + * Default tenant used by tests. Created at setUp; flushed after. + * Auto-attached as the X-Tenant-Code header on every JSON request + * so existing tests don't need per-test boilerplate. + */ + protected ?Tenant $defaultTenant = null; + + protected function setUp(): void + { + parent::setUp(); + $this->defaultTenant = Tenant::firstOrCreate( + ['code' => 'TEST-LGU'], + [ + 'name' => 'Test LGU', + 'short_name' => 'Test', + 'status' => Tenant::STATUS_ACTIVE, + ], + ); + Tenancy::setCurrent($this->defaultTenant); + + $this->withHeaders(['X-Tenant-Code' => $this->defaultTenant->code]); + } + + protected function tearDown(): void + { + Tenancy::clear(); + parent::tearDown(); + } }