mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(companies): migrate existing companies off the general manager
Data rescue, not just schema tidying — the order matters: 1. Backfill companies.email from generalManagerEmail before the key is stripped, or companies whose only address lived there stop receiving mail. 2. Backfill ownerName/Email/Phone (now required) from owner, GM, contact person, then the columns; seed etradeManagerName so existing rows read as matching rather than as a false mismatch. 3. Convert poaSameAsOwner: non-forwarders become poaDeclared='no' with their poa* details cleared; forwarders become 'yes' and keep them, so they now owe a DARS letter they were previously waived. 4. Derive poaDeclared for everyone else. 5. Move drafts off the deleted 'personnel' wizard step. 6. Drop the general_manager_* columns and strip gm*/poaSameAsOwner. down() restores the column shape only; the stripped values are gone by design.
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Onboarding revamp: one company, one verified identity.
|
||||
*
|
||||
* The general manager is removed outright — it named who to talk to and gated
|
||||
* nothing — and the company's people become its **owner** (whoever the eTrade
|
||||
* licence names as the business's manager) and its **Power of Attorney**.
|
||||
* Exactly one of them is identity-verified, chosen by the company's own answer
|
||||
* to "does anyone hold power of attorney for you?", stored as
|
||||
* `attributes.poaDeclared`.
|
||||
*
|
||||
* The order below matters — each step depends on data a later step destroys:
|
||||
*
|
||||
* 1. Rescue notification addresses. `companyNotifyEmailExpr` used to fall
|
||||
* through to `attributes->>'generalManagerEmail'`, and `companies.email` was
|
||||
* only ever written for a Fayda-verified owner — so every foreign company
|
||||
* had none and was reached solely through that fallback. Promote it to the
|
||||
* column before the key is stripped, or those companies stop receiving mail
|
||||
* in silence.
|
||||
* 2. Backfill the owner. `ownerName`/`ownerEmail`/`ownerPhone` are now required
|
||||
* onboarding fields; without this every already-onboarded company would
|
||||
* report three missing fields the moment it opened its settings page.
|
||||
* 3. Resolve `poaSameAsOwner`. That flag waived the DARS delegation paper. It
|
||||
* is gone, so the companies holding it must be re-expressed:
|
||||
* - NOT a freight forwarder → "no PoA" (the owner represents themselves,
|
||||
* nothing to delegate). Their PoA details are cleared.
|
||||
* - A freight forwarder → "yes" and details KEPT. A forwarder signs on
|
||||
* other companies' behalf, so a representative is non-negotiable and the
|
||||
* waiver no longer exists. These companies will be asked for a
|
||||
* delegation paper they were previously excused — an intentional,
|
||||
* visible consequence, not an oversight. Count them before deploying.
|
||||
* 4. Derive the declaration for everyone else, from whether PoA details exist.
|
||||
* 5. Move drafts off the deleted "personnel" wizard step.
|
||||
* 6. Only now drop the columns and strip the retired attribute keys.
|
||||
*
|
||||
* Irreversible by design: `down()` restores the columns' shape but cannot
|
||||
* recover the values, and re-deriving `poaSameAsOwner` from `poaDeclared` would
|
||||
* be a guess.
|
||||
*/
|
||||
export class RemoveGeneralManager3390000000000 implements MigrationInterface {
|
||||
name = "RemoveGeneralManager3390000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 1. Rescue the notification address before the key it lives under is gone.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.companies
|
||||
SET email = COALESCE(
|
||||
NULLIF(email, ''),
|
||||
NULLIF(attributes->>'ownerEmail', ''),
|
||||
NULLIF(attributes->>'generalManagerEmail', ''),
|
||||
NULLIF(attributes->>'contactPersonEmail', '')
|
||||
)
|
||||
WHERE COALESCE(email, '') = ''
|
||||
`);
|
||||
|
||||
// 2. Backfill the owner from the best source each company actually has:
|
||||
// its Fayda-verified owner claims (already under owner*), then the general
|
||||
// manager it named, then its contact person. A company with none of these
|
||||
// never finished onboarding, and will be asked on its next visit.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.companies
|
||||
SET attributes = attributes
|
||||
|| jsonb_strip_nulls(jsonb_build_object(
|
||||
'ownerName', COALESCE(
|
||||
NULLIF(attributes->>'ownerName', ''),
|
||||
NULLIF(attributes->>'generalManagerName', ''),
|
||||
NULLIF(attributes->>'contactPersonName', '')
|
||||
),
|
||||
'ownerEmail', COALESCE(
|
||||
NULLIF(attributes->>'ownerEmail', ''),
|
||||
NULLIF(attributes->>'generalManagerEmail', ''),
|
||||
NULLIF(attributes->>'contactPersonEmail', ''),
|
||||
NULLIF(email, '')
|
||||
),
|
||||
'ownerPhone', COALESCE(
|
||||
NULLIF(attributes->>'ownerPhone', ''),
|
||||
NULLIF(attributes->>'generalManagerPhone', ''),
|
||||
NULLIF(attributes->>'contactPersonPhone', ''),
|
||||
NULLIF(phone, '')
|
||||
)
|
||||
))
|
||||
WHERE attributes IS NOT NULL
|
||||
`);
|
||||
|
||||
// 2b. Capture eTrade's manager for the owner-vs-licence check the
|
||||
// backoffice now makes. Nothing stored it before, so the best we have is
|
||||
// the owner name itself — which makes existing companies read as "matches"
|
||||
// rather than as a false mismatch on data nobody ever compared. The value
|
||||
// is refreshed for real on the company's next eTrade lookup.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.companies
|
||||
SET attributes = jsonb_set(
|
||||
attributes, '{etradeManagerName}', to_jsonb(attributes->>'ownerName')
|
||||
)
|
||||
WHERE COALESCE(attributes->>'ownerName', '') <> ''
|
||||
AND attributes->>'etradeManagerName' IS NULL
|
||||
AND COALESCE(licence_number, '') <> ''
|
||||
`);
|
||||
|
||||
// 3a. Owner-represents-themselves, and NOT a forwarder → "no PoA".
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.companies c
|
||||
SET attributes = (c.attributes - 'poaName' - 'poaPhone' - 'poaEmail'
|
||||
- 'poaLocation' - 'poaAddress' - 'poaFaydaSub'
|
||||
- 'poaFaydaVerifiedAt' - 'poaBirthdate' - 'poaGender')
|
||||
|| jsonb_build_object('poaDeclared', 'no')
|
||||
WHERE (c.attributes->>'poaSameAsOwner')::boolean IS TRUE
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM freight.company_profiles cp
|
||||
WHERE cp.company_id = c.id
|
||||
AND cp.type = 'freight_forwarder'
|
||||
AND cp.deleted_at IS NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// 3b. Forwarders keep their representative and lose the waiver.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.companies c
|
||||
SET attributes = c.attributes || jsonb_build_object('poaDeclared', 'yes')
|
||||
WHERE (c.attributes->>'poaSameAsOwner')::boolean IS TRUE
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM freight.company_profiles cp
|
||||
WHERE cp.company_id = c.id
|
||||
AND cp.type = 'freight_forwarder'
|
||||
AND cp.deleted_at IS NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// 4. Everyone else: "yes" if a representative was named or the company is a
|
||||
// forwarder, "no" if it finished onboarding without one. A company still
|
||||
// mid-onboarding is left unanswered — it will be asked, which is the point.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.companies c
|
||||
SET attributes = COALESCE(c.attributes, '{}'::jsonb)
|
||||
|| jsonb_build_object('poaDeclared', 'yes')
|
||||
WHERE c.attributes->>'poaDeclared' IS NULL
|
||||
AND (
|
||||
COALESCE(c.attributes->>'poaName', '') <> ''
|
||||
OR COALESCE(c.attributes->>'poaPhone', '') <> ''
|
||||
OR COALESCE(c.attributes->>'poaEmail', '') <> ''
|
||||
OR COALESCE(c.attributes->>'poaLocation', '') <> ''
|
||||
OR COALESCE(c.attributes->>'poaAddress', '') <> ''
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM freight.company_profiles cp
|
||||
WHERE cp.company_id = c.id
|
||||
AND cp.type = 'freight_forwarder'
|
||||
AND cp.deleted_at IS NULL
|
||||
)
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.companies c
|
||||
SET attributes = COALESCE(c.attributes, '{}'::jsonb)
|
||||
|| jsonb_build_object('poaDeclared', 'no')
|
||||
WHERE c.attributes->>'poaDeclared' IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM freight.external_profiles ep
|
||||
WHERE ep.company_id = c.id
|
||||
AND ep.onboarding_completed = true
|
||||
AND ep.deleted_at IS NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// 5. The "personnel" (general manager) wizard step no longer exists; a
|
||||
// draft resting on it would fall back to the very first step and make the
|
||||
// customer walk the whole wizard again.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.external_profiles
|
||||
SET onboarding_step = 'owner'
|
||||
WHERE onboarding_step = 'personnel'
|
||||
`);
|
||||
|
||||
// 6. Retire the general manager and the flag it shared the model with.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.companies
|
||||
SET attributes = attributes - 'generalManagerName' - 'generalManagerEmail'
|
||||
- 'generalManagerPhone' - 'gmSameAsOwner' - 'gmFaydaSub'
|
||||
- 'gmFaydaVerifiedAt' - 'gmName' - 'gmEmail' - 'gmPhone'
|
||||
- 'gmAddress' - 'gmBirthdate' - 'gmGender'
|
||||
- 'poaSameAsOwner'
|
||||
WHERE attributes IS NOT NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.companies
|
||||
DROP COLUMN IF EXISTS general_manager_name,
|
||||
DROP COLUMN IF EXISTS general_manager_email,
|
||||
DROP COLUMN IF EXISTS general_manager_phone
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the columns' shape only. The values, the `gm*` attributes and the
|
||||
* `poaSameAsOwner` flag are not recoverable — this migration folded them into
|
||||
* `ownerEmail` / `poaDeclared`, and there is no way back that isn't a guess.
|
||||
*/
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.companies
|
||||
ADD COLUMN IF NOT EXISTS general_manager_name varchar(100),
|
||||
ADD COLUMN IF NOT EXISTS general_manager_email varchar(150),
|
||||
ADD COLUMN IF NOT EXISTS general_manager_phone varchar(20)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.external_profiles
|
||||
SET onboarding_step = 'personnel'
|
||||
WHERE onboarding_step = 'owner'
|
||||
`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user