Additional drizzle cleanup. Move scrub transformation to server side.

This commit is contained in:
Patrick Fic
2026-01-20 11:59:39 -08:00
parent 8954147976
commit 27370bba6d
16 changed files with 1589 additions and 1653 deletions

View File

@@ -1,31 +0,0 @@
import { AnyPgColumn, boolean, pgTable, text, timestamp, uuid, index } from 'drizzle-orm/pg-core';
export const shops = pgTable('shops', {
id: uuid('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
esApiKey: text('es_api_key').notNull().unique(),
active: boolean('active').notNull().default(true),
});
export const jobs = pgTable(
'jobs',
{
id: uuid('id').defaultRandom().primaryKey(),
shopId: uuid('shopId')
.references((): AnyPgColumn => shops.id)
.notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
clm_no: text('clm_no'),
ciecaid: text('ciecaid'),
},
(table) => [index('clm_no_idx').on(table.clm_no)]
);
export const joblines = pgTable('joblines', {
id: uuid('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
jobId: uuid('jobId')
.references((): AnyPgColumn => jobs.id)
.notNull(),
line_desc: text('line_desc'),
});

View File

@@ -1,22 +0,0 @@
import { APIGatewayProxyResult } from 'aws-lambda';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { getDb } from '../lib/db';
export const handler = async (): Promise<APIGatewayProxyResult> => {
try {
const db = await getDb();
await migrate(db, { migrationsFolder: 'drizzle' });
return {
statusCode: 200,
body: JSON.stringify({ success: true, message: 'Migrations applied.' }),
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
statusCode: 500,
body: JSON.stringify({ success: false, error: errorMessage }),
};
}
};

View File

@@ -1,94 +0,0 @@
import { APIGatewayProxyResult } from 'aws-lambda';
import { sql } from 'drizzle-orm';
import { getDb } from '../lib/db';
export const handler = async (): Promise<APIGatewayProxyResult> => {
try {
const db = await getDb();
const ping = await db.execute(sql`select 1 as ok`);
const schemaRows = await db.execute(sql`
select
table_schema,
table_name,
column_name,
data_type,
is_nullable,
ordinal_position
from information_schema.columns
where table_schema not in ('pg_catalog', 'information_schema')
order by table_schema, table_name, ordinal_position
`);
type ColumnInfo = {
name: string;
type: string;
nullable: boolean;
position: number;
};
type TableInfo = {
schema: string;
name: string;
columns: ColumnInfo[];
};
const tableMap = new Map<string, TableInfo>();
const rows = (schemaRows as unknown as { rows?: unknown[] })?.rows ?? [];
for (const row of rows as Array<Record<string, unknown>>) {
const tableSchema = String(row.table_schema ?? '');
const tableName = String(row.table_name ?? '');
if (!tableSchema || !tableName) continue;
const key = `${tableSchema}.${tableName}`;
const existing = tableMap.get(key);
const tableInfo: TableInfo = existing ?? {
schema: tableSchema,
name: tableName,
columns: [],
};
tableInfo.columns.push({
name: String(row.column_name ?? ''),
type: String(row.data_type ?? ''),
nullable: String(row.is_nullable ?? '') === 'YES',
position: Number(row.ordinal_position ?? 0),
});
if (!existing) tableMap.set(key, tableInfo);
}
const tables = [...tableMap.values()];
return {
statusCode: 200,
body: JSON.stringify({ success: true, ping, schema: { tables } }),
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const maybeAny = error as unknown as { cause?: unknown; code?: unknown };
const causeMessage =
maybeAny?.cause instanceof Error
? maybeAny.cause.message
: typeof maybeAny?.cause === 'string'
? maybeAny.cause
: undefined;
const causeCode =
typeof (maybeAny?.cause as { code?: unknown } | undefined)?.code === 'string'
? (maybeAny.cause as { code?: string }).code
: typeof maybeAny?.code === 'string'
? maybeAny.code
: undefined;
return {
statusCode: 500,
body: JSON.stringify({
success: false,
error: errorMessage,
cause: causeMessage,
code: causeCode,
}),
};
}
};

View File

@@ -1,23 +1,18 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import axios, { AxiosError } from 'axios';
import FormData from 'form-data';
import { ESJobObject, RawJobDataObject } from '../../../shared/types';
import { transformJobForEstimateScrubber } from '../lib/transformEstimate';
import { getVehicleType } from '../lib/vehicleTypes/vehicleType';
const ES_USER = process.env.ES_USER || '';
const ES_PASSWORD = process.env.ES_PASSWORD || '';
const ES_ENDPOINT = process.env.ES_ENDPOINT || '';
interface Estimate {
v_type?: string;
v_model: string;
clm_no: string;
sendingEntityId?: string;
[key: string]: unknown;
}
interface ScrubRequest {
esApiKey: string;
estimate: Estimate;
rawJob: RawJobDataObject;
}
interface ScrubResponse {
@@ -27,13 +22,16 @@ interface ScrubResponse {
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
const { esApiKey, estimate } = JSON.parse(event.body || '{}') as ScrubRequest;
const { esApiKey, rawJob } = JSON.parse(event.body || '{}') as ScrubRequest;
// Transform the raw job object to ES format
const estimate: ESJobObject = await transformJobForEstimateScrubber(rawJob);
// Set vehicle type and sending entity ID
estimate.v_type = getVehicleType(estimate.v_model).type;
estimate.sendingEntityId = '87330f61-412b-4251-baaa-d026565b23c5';
estimate.v_type = getVehicleType(estimate.v_model || '').type;
estimate.sending_entity_id = '87330f61-412b-4251-baaa-d026565b23c5';
const fileName = `${esApiKey}-${estimate.clm_no}-${Date.now()}`;
const fileName = `${esApiKey}-${rawJob.clm_no}-${Date.now()}`;
const formData = new FormData();
const jsonString = JSON.stringify(estimate);

View File

@@ -1,78 +0,0 @@
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from '../db/schema';
type DbSecret = {
username: string;
password: string;
};
let cachedSecret: DbSecret | undefined;
let cachedPool: Pool | undefined;
let cachedDb: NodePgDatabase<typeof schema> | undefined;
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required env var: ${name}`);
}
return value;
}
async function getDbSecret(): Promise<DbSecret> {
if (cachedSecret) return cachedSecret;
const secretArn = requireEnv('DB_SECRET_ARN');
const client = new SecretsManagerClient({});
const result = await client.send(
new GetSecretValueCommand({
SecretId: secretArn,
})
);
if (!result.SecretString) {
throw new Error('SecretString was empty for DB_SECRET_ARN');
}
const parsed = JSON.parse(result.SecretString) as Partial<DbSecret>;
if (!parsed.username || !parsed.password) {
throw new Error('DB secret missing username/password');
}
cachedSecret = { username: parsed.username, password: parsed.password };
return cachedSecret;
}
export async function getPool(): Promise<Pool> {
if (cachedPool) return cachedPool;
const host = requireEnv('DB_HOST');
const port = Number.parseInt(requireEnv('DB_PORT'), 10);
const database = requireEnv('DB_NAME');
const { username: user, password } = await getDbSecret();
cachedPool = new Pool({
host,
port,
database,
user,
password,
ssl: {
rejectUnauthorized: true,
},
max: 5,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 10_000,
});
return cachedPool;
}
export async function getDb(): Promise<NodePgDatabase<typeof schema>> {
if (cachedDb) return cachedDb;
const pool = await getPool();
cachedDb = drizzle(pool, { schema });
return cachedDb;
}

View File

@@ -0,0 +1,239 @@
import _ from 'lodash';
import { RawJobDataObject, ESJobObject } from '../../../shared/types';
export async function transformJobForEstimateScrubber(job: RawJobDataObject): Promise<ESJobObject> {
// Take the job object and strip off everything we don't need.
const omittedJob = _.omit(job, [
'cat_no',
'ciecaid',
'agt_co_id',
'agt_co_nm',
'agt_addr1',
'agt_addr2',
'agt_city',
'agt_st',
'agt_zip',
'agt_ctry',
'agt_ph1',
'agt_ph1x',
'agt_ph2',
'agt_ph2x',
'agt_fax',
'agt_faxx',
'agt_ct_ln',
'agt_ct_fn',
'agt_ct_ph',
'agt_ct_phx',
'agt_ea',
'agt_lic_no',
'adj_g_disc',
'adj_strdis',
'adj_towdis',
'asgn_date',
'asgn_no',
'asgn_type',
'clm_addr1',
'clm_addr2',
'clm_city',
'clm_ct_fn',
'clm_ct_ln',
'clm_ct_ph',
'clm_ct_phx',
'clm_ctry',
'clm_ea',
'clm_fax',
'clm_faxx',
'clm_ofc_id',
'clm_ofc_nm',
'clm_ph1',
'clm_ph1x',
'clm_ph2',
'clm_ph2x',
'clm_st',
'clm_title',
'clm_zip',
'cust_pr',
'date_estimated',
'ded_status',
'depreciation_taxes',
'est_addr1',
'est_addr2',
'est_city',
'est_co_nm',
'est_ct_fn',
'est_ct_ln',
'est_ctry',
'est_ea',
'est_ph1',
'est_st',
'est_zip',
'federal_tax_rate',
'ins_addr1',
'ins_addr2',
'ins_city',
'ins_co_id',
'ins_ct_fn',
'ins_ct_ln',
'ins_ct_ph',
'ins_ct_phx',
'ins_ctry',
'ins_ea',
'ins_fax',
'ins_faxx',
'ins_ph1',
'ins_ph1x',
'ins_ph2',
'ins_ph2x',
'ins_st',
'ins_title',
'ins_zip',
'insd_fax',
'insd_faxx',
'kmin',
'loss_cat',
'loss_type',
'ownr_addr2',
'ownr_co_nm',
'ownr_ctry',
'ownr_ea',
'ownr_ph2',
'ownr_st',
'ownr_title',
'ownr_zip',
'pay_amt',
'pay_chknm',
'pay_date',
'pay_type',
'payee_nms',
'plate_no',
'plate_st',
'policy_no',
'rate_la1',
'rate_la2',
'rate_la3',
'rate_la4',
'rate_laa',
'rate_lab',
'rate_lad',
'rate_lae',
'rate_laf',
'rate_lag',
'rate_lam',
'rate_lar',
'rate_las',
'rate_lau',
'rate_ma2s',
'rate_ma2t',
'rate_ma3s',
'rate_mabl',
'rate_macs',
'rate_mahw',
'rate_mapa',
'rate_mash',
'tax_lbr_rt',
'tax_levies_rt',
'tax_paint_mat_rt',
'tax_predis',
'tax_prethr',
'tax_pstthr',
'tax_shop_mat_rt',
'tax_str_rt',
'tax_sub_rt',
'tax_thramt',
'tax_tow_rt',
'theft_ind',
'v_color',
'tlos_ind',
'v_make_desc',
'v_model_desc',
'shopid',
'est_system',
// Object fields
'owner',
'vehicle',
'bodyshop',
'area_of_damage',
'joblines',
'clm_total',
// CIECA Fields
'cieca_pft',
'cieca_pfl',
'cieca_pfm',
'cieca_pfo',
'cieca_stl',
'cieca_ttl',
'parts_tax_rates',
'materials',
]);
// Apply the ES specific transformations needed.
const rates = [
...(job.cieca_pfm?.map((pfm) => ({
cal_prethr: pfm.cal_prethr,
mat_type: pfm.matl_type, // Rename required - presumed typo in ES API.
})) || []),
...(Object.keys(job.cieca_pfl || {}).map((key) =>
_.pick(job.cieca_pfl![key], ['lbr_desc', 'lbr_rate', 'lbr_type'])
) || []),
];
const supp_amt = job.cieca_ttl?.data?.supp_amt || 0;
const totals =
job.cieca_stl?.data?.map((ttl) =>
_.pick(ttl, ['nt_hrs', 't_amt', 't_hrs', 'ttl_typecd'])
) || [];
const now = new Date().toISOString();
return {
...omittedJob,
impact_1: job.area_of_damage?.impact1,
impact_2: job.area_of_damage?.impact2,
close_date: null,
created_at: now,
id: '00000000-0000-0000-0000-000000000000', // Placeholder ID
updated_at: now,
v_age: -1, // Needed? RPS calc.
v_type: 'TBD', // Will be set by caller
v_makedesc: job.v_make_desc,
v_model: job.v_model_desc,
rates,
supp_amt,
totals,
ro_number: null,
requires_reimport: false,
v_mileage: job.kmin?.toString() || '',
sending_entity_id: '',
sending_entity_accept_terms_of_use: true,
association_switch: 'ATAM',
rf_ph1: '2043792253',
rf_zip: 'R0G 1Z0',
g_ttl_amt: job.clm_total || 0,
source_system: job.est_system || 'M',
joblines:
job.joblines?.data?.map((line) => ({
..._.omit(line, [
'lbr_tax',
'lbr_typ_j',
'line_ref',
'misc_sublt',
'misc_tax',
'prt_dsmk_m',
'prt_dsmk_p',
'tran_code',
'unq_seq',
'alt_co_id',
'alt_overrd',
'alt_part_i',
'alt_partm',
'bett_type',
'bett_pctg',
'bett_amt',
'bett_tax',
'op_code_desc',
'paint_stg',
'paint_tone',
]),
})) || [],
} as ESJobObject;
}