Skip to content

Last updated:

How-To: Add a Legacy-Import Mapping Type ​

The legacy importer translates messy free-text from the old system into clean references in the new schema. Each "mapping type" is one column being normalized β€” e.g. doctor names β†’ doctors.id, free-text nationality β†’ ISO codes like phl.

Source of truth: LegacyImportMappingController::TYPE_CONFIG in app/Http/Controllers/Admin/LegacyImportMappingController.php.

Two kinds of mappings ​

KindResolves toExamplesBacking
fkA row in another table ({model}_id)doctor, procedure, medicine, bill_item, insurance, surgery_locationAn Eloquent model
valueA canonical string codenationality (phl), occupation (ACC)A Support\* class

Use fk when the canonical value is a record. Use value when the canonical value is a string code listed in a Support\* class (e.g. App\Support\Nationalities::CODES).

Add an fk mapping type ​

Example: adding a referring_clinic mapping that resolves to referring_clinics.id.

php
// app/Http/Controllers/Admin/LegacyImportMappingController.php
private const TYPE_CONFIG = [
    // …existing types…
    'referring_clinic' => [
        'kind'           => 'fk',
        'label'          => 'Referring Clinics',
        'model'          => \App\Models\ReferringClinic::class,
        'display'        => 'name',                  // column used as the label
        'affected_table' => 'patient_referrals',     // (informational) where the FK lives
        'affected_field' => 'referring_clinic_id',
    ],
];

The typeahead options endpoint (legacy-import.mappings.target-options) reads display to render labels and primary key to map to IDs β€” no extra wiring needed.

Add a value mapping type ​

Example: adding civil_status (single-character codes).

  1. Create the Support class:
php
// app/Support/CivilStatuses.php
namespace App\Support;

class CivilStatuses
{
    public const CODES = ['S', 'M', 'W', 'D', 'A', 'P'];

    public const LABELS = [
        'S' => 'Single',
        'M' => 'Married',
        'W' => 'Widowed',
        'D' => 'Divorced',
        'A' => 'Annulled',
        'P' => 'Separated',
    ];

    public static function label(string $code): string
    {
        return self::LABELS[strtoupper($code)] ?? $code;
    }

    public static function selectOptions(): array
    {
        return collect(self::LABELS)
            ->map(fn ($label, $code) => ['code' => $code, 'label' => $label])
            ->values()
            ->all();
    }
}
  1. Register the mapping type:
php
'civil_status' => [
    'kind'           => 'value',
    'label'          => 'Civil Status',
    'model'          => null,
    'display'        => null,
    'affected_table' => null,
    'affected_field' => 'civil_status',
],
  1. Teach the typeahead endpoint about the new value source. Look for the value branch in targetOptions() β€” add a case for the new type that returns options from your Support class.

  2. Seed initial mappings so common free-text variants are pre-resolved:

php
// database/seeders/CivilStatusMappingSeeder.php
$mappings = [
    'single' => 'S',
    'unmarried' => 'S',
    'married' => 'M',
    'widow' => 'W',
    'widower' => 'W',
    'divorced' => 'D',
    'separated' => 'P',
    'annulled' => 'A',
];

foreach ($mappings as $source => $target) {
    LegacyImportMapping::updateOrCreate(
        ['type' => 'civil_status', 'source_value' => $source],
        ['target_value' => $target, 'resolved' => true, 'auto_created' => false],
    );
}
  1. Use the mapping in the importer:
php
// app/Services/Import/PatientImporter.php
$civilStatus = LegacyImportMapping::resolve('civil_status', $row['civil_status'])
    ?? null;

Add a permission gate ​

The data-mappings page has its own permission (data-mappings.access) so non-admin staff can clean up mappings without touching the rest of the admin panel. Make sure your role assignments include it β€” see Add a Permission.

Test the mapping type ​

php
it('resolves civil status free-text to a canonical code via mapping', function () {
    LegacyImportMapping::factory()->create([
        'type' => 'civil_status',
        'source_value' => 'separated',
        'target_value' => 'P',
        'resolved' => true,
    ]);

    expect(LegacyImportMapping::resolve('civil_status', 'separated'))->toBe('P');
});

Source-byte encoding gotcha ​

The legacy MS Access export is Windows-1252, not UTF-8. The importer decodes source bytes so characters like Γ± and Γ© survive into the mapping table.

If your new column might contain accented characters, run the importer end-to-end with a sample CSV that includes them before declaring victory. See commit 714e506 fix(legacy-import): decode Windows-1252 source bytes so accents survive.

Checklist ​

  • [ ] Type slug is snake_case and matches the new column
  • [ ] kind, label, model/display populated correctly
  • [ ] For value types: Support class with CODES, LABELS, label(), selectOptions()
  • [ ] targetOptions() returns options for the new type
  • [ ] Seeded with common free-text variants
  • [ ] Importer calls LegacyImportMapping::resolve(...) for the column
  • [ ] Tested with an accented-character sample if applicable

CPR - Clinical Patient Records