# PropertySifu MY catalogue migration runbook

This runbook imports the rich Malaysia project data currently stored by
petaV2 in `ih_my_projects` and `ih_my_layouts` into petaV3's canonical
`catalog_*` tables.

It covers project facts, facilities, amenities, prices, floor plans and their
price/PSF/rent/ROI evidence, gallery and document media, and building
identities. The import is idempotent and does not delete any legacy table.

## Safety rules

- Take the approved petaV3 database backup before starting.
- The `reference` connection must use a **SELECT-only** petaV2 database user.
- Do not run this first migration with `--full`. A normal run imports every
  active source row without stale-source deactivation.
- Stop if any command reports a failed record.
- Do not delete `properties`, `property_details`, `property_floor_plans`,
  `property_layout_types`, `fields_address`, `ih_my_projects`, or
  `ih_my_layouts` as part of this procedure.

## 1. Confirm the deployed code and reference connection

Record the deployed revision:

```bash
git rev-parse --short HEAD
```

The petaV3 environment must contain the read-only petaV2 connection:

```dotenv
REFERENCE_DB_HOST=...
REFERENCE_DB_PORT=3306
REFERENCE_DB_DATABASE=peta_wk_dev
REFERENCE_DB_USERNAME=<select-only-user>
REFERENCE_DB_PASSWORD=...
```

After changing environment values, rebuild Laravel's cached configuration
using the deployment's normal config-cache procedure.

Confirm both source tables are readable without printing their data:

```bash
php artisan tinker --execute="dump([
    'projects_table' => \Illuminate\Support\Facades\Schema::connection('reference')->hasTable('ih_my_projects'),
    'layouts_table' => \Illuminate\Support\Facades\Schema::connection('reference')->hasTable('ih_my_layouts'),
    'active_projects' => \Illuminate\Support\Facades\DB::connection('reference')->table('ih_my_projects')->where('isActive', 1)->count(),
    'layouts' => \Illuminate\Support\Facades\DB::connection('reference')->table('ih_my_layouts')->count(),
]);"
```

Expected on the source snapshot verified on 2026-07-28:

- `projects_table`: `true`
- `layouts_table`: `true`
- `active_projects`: `301`
- `layouts`: `1768`

Counts may increase as petaV2 collects new data, but they must not
unexpectedly decrease.

## 2. Apply the schema/provider migration

```bash
php artisan migrate --force
php artisan migrate:status --no-ansi
```

Confirm that
`2026_07_28_100001_seed_propertysifu_provider` is marked as `Ran`.

## 3. Run a one-project canary

```bash
php artisan catalogue:sync propertysifu --limit=1 --no-interaction
```

The command must finish with:

- status `success`;
- `1 created` or `1 updated`;
- `0 failed`;
- stale marking skipped because a limited run is not a full snapshot.

If it fails, stop here. Do not continue to the complete import.

## 4. Import every active PropertySifu project

Run without `--limit` and without `--full`:

```bash
php artisan catalogue:sync propertysifu --no-interaction
```

This consumes all active `ih_my_projects` rows and the matching
`ih_my_layouts` rows. Existing PropertySifu source identities are updated
in place; rerunning the command does not duplicate projects, plans, media, or
buildings.

Check the latest run without exposing source payloads:

```bash
php artisan tinker --execute="dump(
    \Illuminate\Support\Facades\DB::table('catalog_sync_runs')
        ->where('provider_code', 'propertysifu')
        ->latest('id')
        ->first(['id', 'status', 'stats', 'started_at', 'finished_at'])
);"
```

Do not proceed unless `status` is `success` and the `failed` count in `stats`
is zero.

## 5. Reconcile canonical developer links

The import can add new `catalog_projects.developer` values. Rebuild the
canonical developer pivots after the complete sync:

```bash
php artisan catalogue:backfill-redesign --section=developers --no-interaction
```

This command is deterministic and idempotent. It is also the supported repair
for an audit message such as:

```text
catalog_projects/<id> developer string does not match its canonical links.
```

## 6. Verify the migrated catalogue

Check that the provider contributed project, floor-plan, media, and building
rows:

```bash
php artisan tinker --execute="dump([
    'projects' => \Illuminate\Support\Facades\DB::table('catalog_project_sources as source')
        ->join('data_providers as provider', 'provider.id', '=', 'source.data_provider_id')
        ->where('provider.code', 'propertysifu')->count(),
    'floor_plans' => \Illuminate\Support\Facades\DB::table('catalog_floor_plan_sources as plan_source')
        ->join('catalog_project_sources as source', 'source.id', '=', 'plan_source.catalog_project_source_id')
        ->join('data_providers as provider', 'provider.id', '=', 'source.data_provider_id')
        ->where('provider.code', 'propertysifu')->count(),
    'media' => \Illuminate\Support\Facades\DB::table('catalog_media as media')
        ->join('catalog_project_sources as source', 'source.id', '=', 'media.catalog_project_source_id')
        ->join('data_providers as provider', 'provider.id', '=', 'source.data_provider_id')
        ->where('provider.code', 'propertysifu')->count(),
    'buildings' => \Illuminate\Support\Facades\DB::table('catalog_building_sources as building_source')
        ->join('catalog_project_sources as source', 'source.id', '=', 'building_source.catalog_project_source_id')
        ->join('data_providers as provider', 'provider.id', '=', 'source.data_provider_id')
        ->where('provider.code', 'propertysifu')->count(),
]);"
```

Then run the full redesign audit:

```bash
php artisan catalogue:audit-redesign --no-interaction
```

Completion requires:

```text
AUDIT PASSED — every reconciliation gate is green.
```

⚠️ **Nothing goes public on its own — publish before you verify.** Since 2026-08-06 the
`propertysifu` provider deliberately carries **no `publish_on_ingest`**, so `catalogue:sync`
never stamps `published_at`. An unpublished record makes the detail page return null for anyone
without `VIEW_PROJECTS`, so checking the URL below while signed in as an admin proves nothing
about what a visitor sees. Publish the complete ones first:

```bash
php artisan catalogue:completeness                     # what is blocking, per market
php artisan catalogue:propertysifu-publish-complete    # publish the complete MY rows
```

Then verify the known project page and its key sections:

```text
/my/projects/sunway-velocity-2?tab=overview
```

The page must show its project facts, floor plans/layout evidence, media, and
building data. `/analyze-property` must also remain functional. **Check it signed OUT**, or in
a private window — that is the only way to prove publication actually happened.

## 7. Reruns and rollback

For later petaV2 source updates, rerun:

```bash
php artisan catalogue:sync propertysifu --no-interaction
php artisan catalogue:backfill-redesign --section=developers --no-interaction
php artisan catalogue:audit-redesign --no-interaction
```

No additional code deployment is required for these reruns.

If the canary or complete run fails, keep the legacy data and reference
connection in place, capture the failed `catalog_sync_runs` id, and restore the
approved petaV3 backup if the operator decides to roll back. Do not use
`--full` or delete legacy tables as a recovery shortcut.
