- Add TaskAudit/TaskAuditStatus Prisma models linked to Client/Task/User.
- Add ImageRightClient (auth, findFilesByFileNumber, findDocuments,
getSortedFolders) per Vertafore REST v1 API.
- Add audit engine that resolves per-item target dates/windows from the
SHAPE_IR_Filing_Audit_Spec.md checklist, navigates real ImageRight folder
structure, and links results to existing Tasks where found.
- Add GET/POST /api/clients/[id]/task-audit (SHAPE/SHAPE2-gated) and a
Document Audit tab in the client detail page.
- Add Client.amsCustomerNumber (AFW_Customer.CustNo) — ImageRight's
FileNumberPart1 expects this AMS360 account number, not the amsCustomerId
GUID. Backfilled via one-off /api/admin/backfill-ams-customer-number.
- Fix folder/document traversal against verified live ImageRight data:
real folders are nested under a per-year "Policy Term" folder (not flat
SUBMISSION/PRERENEWAL as in the source spec doc), and document listing
requires POST /api/documents/find with {FileId, ParentId} — the
previously assumed /api/containers/{id} endpoint 404s on real ids.
- Add jest coverage for client auth/find calls and folder/doc-type matching.
Known open items (need process-owner input, not resolved here):
- Real IR document types (Correspondences, Pre-Renewal Information, Loss
Run) don't map 1:1 to the spec's generic labels (EMAIL, EXCEL DOC, PDF).
- Spec's second folder_path segment (e.g. PRERENEWAL) has no matching real
subfolder for at least one tested client; currently ignored for folder
navigation and only surfaced in the audit result label.
- Whether an audit item should be skipped when no open Task exists for it,
rather than always running and reporting INCOMPLETE.
147 lines
4.1 KiB
TypeScript
147 lines
4.1 KiB
TypeScript
import { AfwCustomer, AfwPolicy, AfwEmployee } from './afw-queries'
|
|
|
|
/**
|
|
* Map AFW Customer to local Client data
|
|
*/
|
|
export function mapAfwCustomerToClient(
|
|
afwCustomer: AfwCustomer
|
|
): any {
|
|
// Determine customer name: use FirmNameCust for businesses, or construct from LastName/FirstName for individuals
|
|
let customerName = afwCustomer.FirmNameCust
|
|
if (!customerName && (afwCustomer.LastName || afwCustomer.FirstName)) {
|
|
const parts = [afwCustomer.LastName, afwCustomer.FirstName].filter(Boolean)
|
|
customerName = parts.join(', ')
|
|
}
|
|
if (!customerName) {
|
|
customerName = 'Unknown Customer'
|
|
}
|
|
|
|
return {
|
|
amsCustomerId: afwCustomer.CustId,
|
|
amsCustomerNumber: afwCustomer.CustNo,
|
|
name: customerName,
|
|
addressLine1: afwCustomer.Addr1,
|
|
addressLine2: afwCustomer.Addr2,
|
|
city: afwCustomer.City,
|
|
state: afwCustomer.State,
|
|
zipCode: afwCustomer.ZipCode,
|
|
phone: afwCustomer.BusPhone,
|
|
email: afwCustomer.EMail,
|
|
producerCode: afwCustomer.Prod1Code,
|
|
amsCreatedAt: null,
|
|
amsModifiedAt: afwCustomer.ChangedDate,
|
|
lastSyncedAt: new Date(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map AFW Policy to local Policy data (with enriched fields)
|
|
*/
|
|
export function mapAfwPolicyToPolicy(
|
|
afwPolicy: AfwPolicy,
|
|
clientId: string
|
|
): any {
|
|
return {
|
|
amsPolicyId: afwPolicy.PolId,
|
|
client: {
|
|
connect: { id: clientId },
|
|
},
|
|
policyNumber: afwPolicy.PolNo,
|
|
policyType: afwPolicy.PolTypeLOB,
|
|
effectiveDate: afwPolicy.PolEffDate,
|
|
expirationDate: afwPolicy.PolExpDate,
|
|
carrierName: afwPolicy.ParentCompanyName,
|
|
writingCompanyName: afwPolicy.WritingCompanyName,
|
|
premiumAmount: null, // Not in enriched query
|
|
status: afwPolicy.Status,
|
|
billMethod: afwPolicy.BillMethod,
|
|
businessType: afwPolicy.BusinessType,
|
|
department: afwPolicy.PolicyDepartment,
|
|
executiveName: afwPolicy.ExecFormattedName,
|
|
csrName: afwPolicy.CsrFormattedName,
|
|
additionalRep1: afwPolicy.AddRep1FormattedName,
|
|
additionalRep2: afwPolicy.AddRep2FormattedName,
|
|
additionalExec1: afwPolicy.AddExec1FormattedName,
|
|
additionalExec2: afwPolicy.AddExec2FormattedName,
|
|
amsCreatedAt: null,
|
|
amsModifiedAt: null,
|
|
lastSyncedAt: new Date(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map AFW Employee to local User data
|
|
*/
|
|
export function mapAfwEmployeeToUser(
|
|
afwEmployee: AfwEmployee
|
|
): any {
|
|
const displayName = [afwEmployee.FirstName, afwEmployee.LastName]
|
|
.filter(Boolean)
|
|
.join(' ') || afwEmployee.ShortName || afwEmployee.EmpCode
|
|
|
|
return {
|
|
email: afwEmployee.EMail || `${afwEmployee.EmpCode}@placeholder.local`,
|
|
displayName,
|
|
department: afwEmployee.DefaultGLDeptCode,
|
|
isActive: afwEmployee.Status === 'A',
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize phone number format
|
|
*/
|
|
export function normalizePhoneNumber(phone: string | null): string | null {
|
|
if (!phone) return null
|
|
|
|
// Remove all non-digit characters
|
|
const digits = phone.replace(/\D/g, '')
|
|
|
|
// Format as (XXX) XXX-XXXX if 10 digits
|
|
if (digits.length === 10) {
|
|
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`
|
|
}
|
|
|
|
return phone
|
|
}
|
|
|
|
/**
|
|
* Normalize email format
|
|
*/
|
|
export function normalizeEmail(email: string | null): string | null {
|
|
if (!email) return null
|
|
return email.toLowerCase().trim()
|
|
}
|
|
|
|
/**
|
|
* Determine if a record should be updated based on modification dates
|
|
*/
|
|
export function shouldUpdateRecord(
|
|
localModifiedAt: Date | null,
|
|
afwModifiedAt: Date | null
|
|
): boolean {
|
|
if (!localModifiedAt) return true
|
|
if (!afwModifiedAt) return false
|
|
|
|
return afwModifiedAt > localModifiedAt
|
|
}
|
|
|
|
/**
|
|
* Calculate days until policy expiration
|
|
*/
|
|
export function calculateDaysUntilExpiration(expirationDate: Date): number {
|
|
const now = new Date()
|
|
const expDate = new Date(expirationDate)
|
|
const diffTime = expDate.getTime() - now.getTime()
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|
}
|
|
|
|
/**
|
|
* Determine if a policy is eligible for renewal workflow
|
|
*/
|
|
export function isPolicyEligibleForRenewal(
|
|
expirationDate: Date,
|
|
daysThreshold: number = 90
|
|
): boolean {
|
|
const daysUntil = calculateDaysUntilExpiration(expirationDate)
|
|
return daysUntil > 0 && daysUntil <= daysThreshold
|
|
}
|