2026-02-18 13:20:52 +00:00
|
|
|
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,
|
feat(imageright): Task Audit feature for SHAPE clients
- 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.
2026-07-08 10:39:12 +00:00
|
|
|
amsCustomerNumber: afwCustomer.CustNo,
|
2026-02-18 13:20:52 +00:00
|
|
|
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
|
|
|
|
|
}
|