# GSD: Update EnrichAssetFromAPI — Innomaint IDs + Geo Fields
## File: `cmms_import.go`

---

## WHAT TO UPDATE

`EnrichAssetFromAPI` needs to:
1. Store Innomaint's real IDs in all master tables (`innomaint_customer_id`, `innomaint_location_id`, etc.)
2. Store geo lat/lng for customer, location, building
3. Store missing asset_master fields (qrcode, amc_id, contract_type, is_main_asset, etc.)
4. Update ic3_equipment_master with description + is_movable from assetIDDetails
5. Resolve local FKs using Innomaint IDs (faster than name lookup)

---

## STEP 1 — Update InnomaintAssetData struct

Find `InnomaintAssetData` struct and ADD these missing fields:

```go
type InnomaintAssetData struct {
    // ... existing fields ...

    // Geo fields
    BuildingGeoLocation   string  `json:"building_geo_location"`
    BuildingGeoLattitude  string  `json:"building_geo_lattitude"`
    BuildingGeoLongitude  string  `json:"building_geo_longitude"`
    GeoLocation           string  `json:"geo_location"`
    GeoLattitude          *string `json:"geo_lattitude"`
    GeoLongitude          *string `json:"geo_longitude"`
    LocationGeoAddress    string  `json:"location_geo_address"`
    LocationGeoLattitude  *string `json:"location_geo_lattitude"`
    LocationGeoLongitude  *string `json:"location_geo_longitude"`
    CustomerLattitude     *string `json:"customer_lattitude"`
    CustomerLongitude     *string `json:"customer_longitude"`
    CustomerGeoAddress    *string `json:"customer_geo_address"`

    // Additional fields
    AssetReferenceNumber  string  `json:"asset_reference_number"`
    LoanerAvailability    int     `json:"loaner_availability"`
    SelectMachineOwner    *int    `json:"selectmachineowner"`
    SelectMachineOwnerName string `json:"selectmachineownername"`
    EquipmentsModelID     int     `json:"equipments_model_id"`
}
```

Also ADD `InnomaintAssetIDDetails` struct for the `assetIDDetails` block:

```go
type InnomaintAssetIDDetails struct {
    FkCompanyID           int     `json:"fk_company_id"`
    ModelID               int     `json:"model_id"`
    AssetID               int     `json:"asset_id"`
    ManufacturerID        int     `json:"manufacturer_id"`
    ManufacturerName      string  `json:"manufacturer_name"`
    CatID                 int     `json:"cat_id"`
    CategoryName          string  `json:"category_name"`
    SubcategoryID         int     `json:"subcategory_id"`
    SubCategory           string  `json:"sub_category"`
    EquipmentsName        string  `json:"equipments_name"`
    EquipmentModelName    string  `json:"equipment_model_name"`
    EquipmentDescription  string  `json:"equipment_description"`
    IsMovable             int     `json:"is_movable"`
    AмcID                 int     `json:"amc_id"`
    ContractType          string  `json:"contract_type"`
    IsMainAsset           int     `json:"is_main_asset"`
    IsFacility            int     `json:"is_facility"`
    ApprovalRequired      int     `json:"approval_required"`
    WorkorderRequired     int     `json:"workorder_required"`
    CustomerWorkorderApproval int `json:"customer_workorder_approval"`
    Qrcode                string  `json:"qrcode"`
    QrcodeImage           string  `json:"qrcode_image"`
    TraceabilityUpdatedOn string  `json:"traceability_updated_on"`
    AssetBuildingID       int     `json:"asset_building_id"`
    AssetDepartmentID     int     `json:"asset_department_id"`
    BuildingGeoLattitude  string  `json:"building_geo_lattitude"`
    BuildingGeoLongitude  string  `json:"building_geo_longitude"`
    BuildingGeoLocation   string  `json:"building_geo_location"`
    CustomerID            int     `json:"customer_id"`
    CustomerName          string  `json:"customer_name"`
    LocationID            int     `json:"location_id"`
    LocationName          string  `json:"location_name"`
    BuildingID            int     `json:"building_id"`
    BuildingName          string  `json:"building_name"`
    FloorID               int     `json:"floor_id"`
    FloorName             string  `json:"floor_name"`
    DepartmentID          int     `json:"department_id"`
    DepartmentName        string  `json:"department_name"`
}
```

Update `InnomaintResponseBody` to include assetIDDetails:

```go
type InnomaintResponseBody struct {
    Data                   InnomaintAssetData      `json:"data"`
    Performance            InnomaintPerformance    `json:"performance"`
    WorkorderTransactional InnomaintWorkorder      `json:"workorder_transactional"`
    AssetIDDetails         InnomaintAssetIDDetails `json:"assetIDDeatails"` // note: typo in API
}
```

---

## STEP 2 — Replace entire EnrichAssetFromAPI function

Find `func (db *DB) EnrichAssetFromAPI` and replace the ENTIRE function with:

```go
func (db *DB) EnrichAssetFromAPI(ctx context.Context, tx pgx.Tx, apiResp *InnomaintAPIResponse) error {
    d := apiResp.Response.Data
    p := apiResp.Response.Performance
    wo := apiResp.Response.WorkorderTransactional
    det := apiResp.Response.AssetIDDetails

    // ----------------------------------------------------------------
    // 1. Upsert ic3_customer_master — store innomaint_customer_id + geo
    // ----------------------------------------------------------------
    var localCustID *int
    if d.CustomerID > 0 && d.CustomerName != "" {
        var id int
        err := tx.QueryRow(ctx, `
            INSERT INTO ic3_customer_master (
                customer_name, customer_status, source_system_id,
                innomaint_customer_id,
                geo_lattitude, geo_longitude, geo_address,
                created_at, updated_at
            ) VALUES ($1, 1, 'INNOMAINT', $2, $3, $4, $5, NOW(), NOW())
            ON CONFLICT (customer_name) DO UPDATE SET
                innomaint_customer_id = EXCLUDED.innomaint_customer_id,
                geo_lattitude         = COALESCE(EXCLUDED.geo_lattitude, ic3_customer_master.geo_lattitude),
                geo_longitude         = COALESCE(EXCLUDED.geo_longitude, ic3_customer_master.geo_longitude),
                geo_address           = COALESCE(EXCLUDED.geo_address,   ic3_customer_master.geo_address),
                updated_at            = NOW()
            RETURNING customer_id`,
            d.CustomerName,
            d.CustomerID,
            nullStr(ptrStr(d.CustomerLattitude)),
            nullStr(ptrStr(d.CustomerLongitude)),
            nullStr(ptrStr(d.CustomerGeoAddress)),
        ).Scan(&id)
        if err != nil {
            return fmt.Errorf("upsert customer: %w", err)
        }
        localCustID = &id
    } else {
        tx.QueryRow(ctx,
            `SELECT customer_id FROM ic3_customer_master WHERE customer_name = $1 LIMIT 1`,
            d.CustomerName,
        ).Scan(&localCustID)
    }

    // ----------------------------------------------------------------
    // 2. Upsert ic3_location_master — store innomaint_location_id + geo
    // ----------------------------------------------------------------
    var localLocID *int
    if d.LocationID > 0 && d.LocationName != "" {
        var id int
        err := tx.QueryRow(ctx, `
            INSERT INTO ic3_location_master (
                location_name, customer_id, location_status, source_system_id,
                innomaint_location_id,
                geo_lattitude, geo_longitude, geo_address,
                created_at, updated_at
            ) VALUES ($1, $2, 1, 'INNOMAINT', $3, $4, $5, $6, NOW(), NOW())
            ON CONFLICT (location_name) DO UPDATE SET
                innomaint_location_id = EXCLUDED.innomaint_location_id,
                customer_id           = COALESCE(EXCLUDED.customer_id, ic3_location_master.customer_id),
                geo_lattitude         = COALESCE(EXCLUDED.geo_lattitude, ic3_location_master.geo_lattitude),
                geo_longitude         = COALESCE(EXCLUDED.geo_longitude, ic3_location_master.geo_longitude),
                geo_address           = COALESCE(EXCLUDED.geo_address,   ic3_location_master.geo_address),
                updated_at            = NOW()
            RETURNING location_id`,
            d.LocationName, localCustID, d.LocationID,
            nullStr(ptrStr(d.LocationGeoLattitude)),
            nullStr(ptrStr(d.LocationGeoLongitude)),
            nullStr(d.LocationGeoAddress),
        ).Scan(&id)
        if err != nil {
            return fmt.Errorf("upsert location: %w", err)
        }
        localLocID = &id
    } else {
        tx.QueryRow(ctx,
            `SELECT location_id FROM ic3_location_master WHERE location_name = $1 LIMIT 1`,
            d.LocationName,
        ).Scan(&localLocID)
    }

    // ----------------------------------------------------------------
    // 3. Upsert ic3_building — innomaint_building_id + geo lat/lng
    // ----------------------------------------------------------------
    if d.BuildingID > 0 && d.BuildingName != "" {
        _, err := tx.Exec(ctx, `
            INSERT INTO ic3_building (
                building_id, customer_id, building_name,
                innomaint_building_id,
                geo_lattitude, geo_longitude, geo_location,
                created_at, updated_at
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
            ON CONFLICT (building_id) DO UPDATE SET
                building_name         = EXCLUDED.building_name,
                innomaint_building_id = EXCLUDED.innomaint_building_id,
                geo_lattitude         = COALESCE(EXCLUDED.geo_lattitude, ic3_building.geo_lattitude),
                geo_longitude         = COALESCE(EXCLUDED.geo_longitude, ic3_building.geo_longitude),
                geo_location          = COALESCE(EXCLUDED.geo_location,  ic3_building.geo_location),
                updated_at            = NOW()`,
            d.BuildingID, localCustID, d.BuildingName,
            d.BuildingID, // innomaint_building_id same as building_id from API
            nullStr(d.BuildingGeoLattitude),
            nullStr(d.BuildingGeoLongitude),
            nullStr(d.BuildingGeoLocation),
        )
        if err != nil {
            return fmt.Errorf("upsert building: %w", err)
        }
    }

    // ----------------------------------------------------------------
    // 4. Upsert ic3_floor_master — innomaint_floor_id
    // ----------------------------------------------------------------
    if d.FloorID > 0 && d.FloorName != "" {
        _, err := tx.Exec(ctx, `
            INSERT INTO ic3_floor_master (
                floor_id, floor_name, location_id,
                asset_floor_id, innomaint_floor_id,
                created_at, updated_at
            ) VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
            ON CONFLICT (floor_id) DO UPDATE SET
                floor_name         = EXCLUDED.floor_name,
                innomaint_floor_id = EXCLUDED.innomaint_floor_id,
                updated_at         = NOW()`,
            d.FloorID, d.FloorName, localLocID,
            nullInt(d.AssetFloorID), d.FloorID,
        )
        if err != nil {
            return fmt.Errorf("upsert floor: %w", err)
        }
    }

    // ----------------------------------------------------------------
    // 5. Upsert ic3_department_master — innomaint_department_id
    // ----------------------------------------------------------------
    if d.DepartmentID > 0 && d.DepartmentName != "" {
        _, err := tx.Exec(ctx, `
            INSERT INTO ic3_department_master (
                department_id, department_name, floor_id, location_id,
                asset_department_id, innomaint_department_id,
                created_at, updated_at
            ) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
            ON CONFLICT (department_id) DO UPDATE SET
                department_name          = EXCLUDED.department_name,
                innomaint_department_id  = EXCLUDED.innomaint_department_id,
                updated_at               = NOW()`,
            d.DepartmentID, d.DepartmentName,
            nullInt(d.FloorID), localLocID,
            nullInt(d.AssetDepartmentID), d.DepartmentID,
        )
        if err != nil {
            return fmt.Errorf("upsert department: %w", err)
        }
    }

    // ----------------------------------------------------------------
    // 6. Upsert ic3_asset_category — innomaint_cat_id
    // ----------------------------------------------------------------
    var localCatID *int
    if det.CatID > 0 && det.CategoryName != "" {
        var id int
        err := tx.QueryRow(ctx, `
            INSERT INTO ic3_asset_category (
                category_name, category_status, innomaint_cat_id, created_at
            ) VALUES ($1, 1, $2, NOW())
            ON CONFLICT (category_name) DO UPDATE SET
                innomaint_cat_id = EXCLUDED.innomaint_cat_id
            RETURNING cat_id`,
            det.CategoryName, det.CatID,
        ).Scan(&id)
        if err != nil {
            return fmt.Errorf("upsert category: %w", err)
        }
        localCatID = &id
    }

    // ----------------------------------------------------------------
    // 7. Upsert ic3_asset_group — innomaint_subcategory_id
    // ----------------------------------------------------------------
    var localGroupID *int
    if det.SubcategoryID > 0 && det.SubCategory != "" {
        var id int
        err := tx.QueryRow(ctx, `
            INSERT INTO ic3_asset_group (
                asset_group_name, cat_id, asset_group_status,
                innomaint_subcategory_id, created_at
            ) VALUES ($1, $2, 1, $3, NOW())
            ON CONFLICT (asset_group_name) DO UPDATE SET
                innomaint_subcategory_id = EXCLUDED.innomaint_subcategory_id,
                cat_id                   = COALESCE(EXCLUDED.cat_id, ic3_asset_group.cat_id)
            RETURNING subcategory_id`,
            det.SubCategory, localCatID, det.SubcategoryID,
        ).Scan(&id)
        if err != nil {
            return fmt.Errorf("upsert asset group: %w", err)
        }
        localGroupID = &id
    }

    // ----------------------------------------------------------------
    // 8. Upsert ic3_manufacturer — innomaint_manufacturer_id
    // ----------------------------------------------------------------
    var localMfrID *int
    if det.ManufacturerID > 0 && det.ManufacturerName != "" {
        var id int
        err := tx.QueryRow(ctx, `
            INSERT INTO ic3_manufacturer (
                manufacturer_name, manufacturer_status,
                innomaint_manufacturer_id, created_at
            ) VALUES ($1, 1, $2, NOW())
            ON CONFLICT (manufacturer_name) DO UPDATE SET
                innomaint_manufacturer_id = EXCLUDED.innomaint_manufacturer_id
            RETURNING manufacturer_id`,
            det.ManufacturerName, det.ManufacturerID,
        ).Scan(&id)
        if err != nil {
            return fmt.Errorf("upsert manufacturer: %w", err)
        }
        localMfrID = &id
    }

    // ----------------------------------------------------------------
    // 9. Upsert ic3_equipment_master — innomaint_model_id + description
    // ----------------------------------------------------------------
    var localModelID *int
    if det.ModelID > 0 && det.EquipmentsName != "" {
        modelName := det.EquipmentModelName
        if modelName == "" {
            modelName = "NA"
        }
        var id int
        err := tx.QueryRow(ctx, `
            INSERT INTO ic3_equipment_master (
                equipments_name, equipment_model_name, equipment_description,
                cat_id, asset_group_id, manufacturer_id, manufacturer_name,
                innomaint_model_id, is_movable, model_status, created_at
            ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,1,NOW())
            ON CONFLICT (equipments_name, equipment_model_name) DO UPDATE SET
                innomaint_model_id    = EXCLUDED.innomaint_model_id,
                equipment_description = COALESCE(EXCLUDED.equipment_description, ic3_equipment_master.equipment_description),
                cat_id                = COALESCE(EXCLUDED.cat_id,          ic3_equipment_master.cat_id),
                asset_group_id        = COALESCE(EXCLUDED.asset_group_id,  ic3_equipment_master.asset_group_id),
                manufacturer_id       = COALESCE(EXCLUDED.manufacturer_id, ic3_equipment_master.manufacturer_id),
                is_movable            = EXCLUDED.is_movable
            RETURNING model_id`,
            det.EquipmentsName, modelName, nullStr(det.EquipmentDescription),
            localCatID, localGroupID, localMfrID, nullStr(det.ManufacturerName),
            det.ModelID, det.IsMovable,
        ).Scan(&id)
        if err != nil {
            return fmt.Errorf("upsert equipment: %w", err)
        }
        localModelID = &id
    }

    // ----------------------------------------------------------------
    // 10. Parse traceability_updated_on timestamp
    // ----------------------------------------------------------------
    var traceUpdatedOn *time.Time
    if det.TraceabilityUpdatedOn != "" {
        layouts := []string{
            "2006-01-02 15:04:05+00",
            "2006-01-02 15:04:05",
            "2006-01-02T15:04:05Z",
        }
        for _, layout := range layouts {
            if t, err := time.Parse(layout, det.TraceabilityUpdatedOn); err == nil {
                traceUpdatedOn = &t
                break
            }
        }
    }

    // ----------------------------------------------------------------
    // 11. UPDATE ic3_asset_master — full enrich with all fields + local FKs
    // ----------------------------------------------------------------
    _, err := tx.Exec(ctx, `
        UPDATE ic3_asset_master SET
            traceability_id                             = $2,
            barcode                                     = $3,
            qrcode                                      = $4,
            qrcode_image                                = $5,
            criticality                                 = $6,
            capacity_rating                             = $7,
            asset_reference_number                      = $8,
            customer_id                                 = COALESCE($9,  customer_id),
            location_id                                 = COALESCE($10, location_id),
            building_id                                 = $11,
            floor_id                                    = $12,
            department_id                               = $13,
            asset_building_id                           = $14,
            asset_floor_id                              = $15,
            asset_department_id                         = $16,
            model_id                                    = COALESCE($17, model_id),
            asset_cat_id                                = COALESCE($18, asset_cat_id),
            asset_group_id                              = COALESCE($19, asset_group_id),
            fk_customers_equipment_mapping_id           = $20,
            customers_locations_serialnumber_mapping_id = $21,
            customer_traceability_id                    = $22,
            customer_model_id                           = $23,
            location_traceability_id                    = $24,
            location_model_id                           = $25,
            in_charge_person                            = $26,
            ticketcount                                 = $27,
            schedulecount                               = $28,
            schedule_configured                         = $29,
            schedule_initiate                           = $30,
            iot_device_mapped                           = $31,
            status                                      = $32,
            asset_live_status                           = $33,
            mapping_status                              = $34,
            traceability_status                         = $35,
            is_loaner                                   = $36,
            loaner_availability                         = $37,
            is_ble_tracking                             = $38,
            is_rfid_tracking                            = $39,
            is_qr_tracking                              = $40,
            is_tracking_enabled                         = $41,
            is_asset_tracking                           = $42,
            is_device_tracking                          = $43,
            asset_tracking_status                       = $44,
            is_main_asset                               = $45,
            is_facility                                 = $46,
            fk_company_id                               = $47,
            approval_required                           = $48,
            workorder_required                          = $49,
            amc_id                                      = $50,
            contract_type                               = $51,
            contract_name                               = $52,
            contract_number                             = $53,
            in_contract_type                            = $54,
            contract_status                             = $55,
            asset_contract_id                           = $56,
            asset_contract_start_date                   = $57,
            asset_contract_end_date                     = $58,
            is_existing_contract                        = $59,
            asset_current_lattitude                     = $60,
            asset_current_longitude                     = $61,
            asset_current_location                      = $62,
            traceability_updated_on                     = $63,
            last_synced_at                              = NOW(),
            sync_version                                = COALESCE(sync_version, 0) + 1,
            updated_at                                  = NOW()
        WHERE cmms_asset_id = $1`,
        d.Serialnumber,           // $1  WHERE key
        d.TraceID,                // $2
        nullStr(d.Barcode),       // $3
        nullStr(det.Qrcode),      // $4
        nullStr(det.QrcodeImage), // $5
        nullStr(d.Criticality),   // $6
        nullStr(d.CapacityRating),// $7
        nullStr(d.AssetReferenceNumber), // $8
        localCustID,              // $9
        localLocID,               // $10
        nullInt(d.BuildingID),    // $11
        nullInt(d.FloorID),       // $12
        nullInt(d.DepartmentID),  // $13
        nullInt(det.AssetBuildingID),    // $14
        nullInt(d.AssetFloorID),         // $15
        nullInt(det.AssetDepartmentID),  // $16
        localModelID,             // $17
        localCatID,               // $18
        localGroupID,             // $19
        nullInt(d.FkCustomersEquipmentMappingID),        // $20
        nullInt(d.CustomersLocationsSerialMappingID),    // $21
        nullInt(d.CustomerTraceabilityID),               // $22
        nullInt(d.CustomerModelID),                      // $23
        nullInt(d.LocationTraceabilityID),               // $24
        nullInt(d.LocationModelID),                      // $25
        nullStr(d.InChargePerson),   // $26
        d.Ticketcount,               // $27
        d.Schedulecount,             // $28
        d.ScheduleConfigured,        // $29
        d.ScheduleInitiate,          // $30
        d.IotDeviceMapped,           // $31
        d.Status,                    // $32
        d.AssetLiveStatus,           // $33
        d.MappingStatus,             // $34
        d.TraceabilityStatus,        // $35
        d.IsLoaner,                  // $36
        d.LoanerAvailability,        // $37
        d.IsBleTracking,             // $38
        d.IsRfidTracking,            // $39
        d.IsQrTracking,              // $40
        d.IsTrackingEnabled,         // $41
        d.IsAssetTracking,           // $42
        d.IsDeviceTracking,          // $43
        d.AssetTrackingStatus,       // $44
        det.IsMainAsset,             // $45
        det.IsFacility,              // $46
        nullInt(det.FkCompanyID),    // $47
        det.ApprovalRequired,        // $48
        det.WorkorderRequired,       // $49
        nullInt(det.AмcID),          // $50
        nullStr(det.ContractType),   // $51
        nullStr(d.ContractName),     // $52
        nullStr(d.ContractNumber),   // $53
        d.InContractType,            // $54
        d.ContractStatus,            // $55
        nullInt(d.AssetContractID),  // $56
        nullStr(d.AssetContractStartDate), // $57
        nullStr(d.AssetContractEndDate),   // $58
        nullInt(d.IsExistingContract),     // $59
        nullStr(ptrStr(d.AssetCurrentLattitude)), // $60
        nullStr(ptrStr(d.AssetCurrentLongitude)), // $61
        nullStr(d.AssetCurrentLocation),          // $62
        traceUpdatedOn,              // $63
    )
    if err != nil {
        return fmt.Errorf("update asset_master: %w", err)
    }

    // ----------------------------------------------------------------
    // 12. Get local asset_id for child table inserts
    // ----------------------------------------------------------------
    var assetID int
    err = tx.QueryRow(ctx,
        `SELECT asset_id FROM ic3_asset_master WHERE cmms_asset_id = $1`,
        d.Serialnumber,
    ).Scan(&assetID)
    if err != nil {
        log.Printf("EnrichAssetFromAPI: asset not found for cmms_asset_id=%q: %v", d.Serialnumber, err)
        return fmt.Errorf("get asset_id for %q: %w", d.Serialnumber, err)
    }

    // ----------------------------------------------------------------
    // 13. Upsert ic3_asset_performance
    // ----------------------------------------------------------------
    _, err = tx.Exec(ctx, `
        INSERT INTO ic3_asset_performance (
            asset_id, serialnumber, equipments_name, equipment_model_name,
            criticality_type_label, total_ticket, mttr, mtbf, availability,
            total_ticket_breakdown_hrs, total_schedule_breakdown_hrs,
            asset_created_date, total_actual_hrs, recorded_at
        ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,NOW())
        ON CONFLICT (asset_id) DO UPDATE SET
            total_ticket                 = EXCLUDED.total_ticket,
            mttr                         = EXCLUDED.mttr,
            mtbf                         = EXCLUDED.mtbf,
            availability                 = EXCLUDED.availability,
            total_ticket_breakdown_hrs   = EXCLUDED.total_ticket_breakdown_hrs,
            total_schedule_breakdown_hrs = EXCLUDED.total_schedule_breakdown_hrs,
            total_actual_hrs             = EXCLUDED.total_actual_hrs,
            recorded_at                  = NOW()`,
        assetID, d.Serialnumber,
        p.EquipmentsName, p.EquipmentModelName, p.CriticalityTypeLabel,
        p.TotalTicket, p.Mttr, p.Mtbf, p.Availability,
        p.TotalTicketBreakdownHrs, p.TotalScheduleBreakdownHrs,
        p.AssetCreatedDate, p.TotalActualHrs,
    )
    if err != nil {
        return fmt.Errorf("upsert performance: %w", err)
    }

    // ----------------------------------------------------------------
    // 14. Upsert ic3_workorder_summary — upto + last_year
    // ----------------------------------------------------------------
    for _, period := range []struct {
        pType string
        wop   InnomaintWOPeriod
    }{
        {"upto", wo.Upto},
        {"last_year", wo.LastYear},
    } {
        weCount, _ := strconv.Atoi(period.wop.WorkEstimateCount)
        weCost, _  := strconv.ParseFloat(period.wop.WorkEstimateCost, 64)
        scCount, _ := strconv.Atoi(period.wop.ScheduleCount)
        scCost, _  := strconv.ParseFloat(period.wop.ScheduleCost, 64)
        tkCount, _ := strconv.Atoi(period.wop.TicketCount)
        tkCost, _  := strconv.ParseFloat(period.wop.TicketCost, 64)

        _, err = tx.Exec(ctx, `
            INSERT INTO ic3_workorder_summary (
                asset_id, period_type,
                work_estimate_count, work_estimate_cost,
                schedule_count, schedule_cost,
                ticket_count, ticket_cost, updated_at
            ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW())
            ON CONFLICT (asset_id, period_type) DO UPDATE SET
                work_estimate_count = EXCLUDED.work_estimate_count,
                work_estimate_cost  = EXCLUDED.work_estimate_cost,
                schedule_count      = EXCLUDED.schedule_count,
                schedule_cost       = EXCLUDED.schedule_cost,
                ticket_count        = EXCLUDED.ticket_count,
                ticket_cost         = EXCLUDED.ticket_cost,
                updated_at          = NOW()`,
            assetID, period.pType,
            weCount, weCost, scCount, scCost, tkCount, tkCost,
        )
        if err != nil {
            return fmt.Errorf("upsert workorder %s: %w", period.pType, err)
        }
    }

    return nil
}
```

---

## STEP 3 — Add ptrStr helper if not already present

Add this helper function anywhere in `cmms_import.go`:

```go
// ptrStr dereferences a *string safely, returns "" if nil
func ptrStr(s *string) string {
    if s == nil {
        return ""
    }
    return *s
}
```

---

## STEP 4 — Add missing fields to InnomaintAssetData struct

These fields are in `response.data` but not yet in the struct. ADD them:

```go
// In InnomaintAssetData struct, add:
AssetReferenceNumber    string  `json:"asset_reference_number"`
LoanerAvailability      int     `json:"loaner_availability"`
SelectMachineOwner      *int    `json:"selectmachineowner"`
SelectMachineOwnerName  string  `json:"selectmachineownername"`
AssetTrackingStatus     int     `json:"asset_tracking_status"`
ContractName            string  `json:"contract_name"`
ContractNumber          string  `json:"contract_number"`
InContractType          int     `json:"in_contract_type"`
ContractStatus          int     `json:"contract_status"`
AssetContractID         *int    `json:"asset_contract_id"`
AssetContractStartDate  string  `json:"asset_contract_start_date"`
AssetContractEndDate    string  `json:"asset_contract_end_date"`
IsExistingContract      *int    `json:"is_existing_contract"`
AssetCurrentLattitude   *string `json:"asset_current_lattitude"`
AssetCurrentLongitude   *string `json:"asset_current_longitude"`
AssetCurrentLocation    string  `json:"asset_current_location"`
BuildingGeoLocation     string  `json:"building_geo_location"`
BuildingGeoLattitude    string  `json:"building_geo_lattitude"`
BuildingGeoLongitude    string  `json:"building_geo_longitude"`
LocationGeoAddress      string  `json:"location_geo_address"`
LocationGeoLattitude    *string `json:"location_geo_lattitude"`
LocationGeoLongitude    *string `json:"location_geo_longitude"`
CustomerLattitude       *string `json:"customer_lattitude"`
CustomerLongitude       *string `json:"customer_longitude"`
CustomerGeoAddress      *string `json:"customer_geo_address"`
```

---

## VERIFY AFTER APPLYING

```bash
# Single asset test
curl -X POST "http://localhost:9090/api/admin/cmms/sync/Delhi%20Cantt%20Naraina_Trans_10" \
  -H "Authorization: Bearer <token>"
```

Then check all tables updated:
```sql
-- Check asset_master enriched fields
SELECT cmms_asset_id, traceability_id, qrcode, criticality,
       customer_id, location_id, building_id, floor_id, department_id,
       asset_cat_id, asset_group_id, model_id,
       amc_id, contract_type, is_main_asset,
       last_synced_at
FROM ic3_asset_master
WHERE cmms_asset_id = 'Delhi Cantt Naraina_Trans_10';

-- Check customer got innomaint_id
SELECT customer_id, customer_name, innomaint_customer_id, geo_lattitude, geo_longitude
FROM ic3_customer_master;

-- Check location got innomaint_id
SELECT location_id, location_name, innomaint_location_id, geo_lattitude, geo_longitude
FROM ic3_location_master;

-- Check category got innomaint_id
SELECT cat_id, category_name, innomaint_cat_id FROM ic3_asset_category;

-- Check asset group got innomaint_id
SELECT subcategory_id, asset_group_name, innomaint_subcategory_id FROM ic3_asset_group;

-- Check building got geo
SELECT building_id, building_name, innomaint_building_id,
       geo_lattitude, geo_longitude, geo_location
FROM ic3_building;

-- Check performance
SELECT * FROM ic3_asset_performance
WHERE asset_id = (SELECT asset_id FROM ic3_asset_master
                  WHERE cmms_asset_id = 'Delhi Cantt Naraina_Trans_10');
```
