seubert-claims/ondeck/src/lib/sync/afw-queries.ts
lorentz 8aa20ea751 feat: sync contacts from AMS 360 AFW_PolContact into ClientContact
- Add source, title, mobilePhone fields to ClientContact schema
- Add composite unique constraint for dedup (clientId, name, label, source)
- Add fetchAfwContacts() query with dedup by (CustId, Name, Responsibility)
- Add syncContacts() phase to sync engine with retry logic
- Update contacts UI to show title, mobile phone, AMS badge
- Disable edit/delete on AMS-synced contacts (source='ams')
2026-04-08 02:08:27 +00:00

463 lines
12 KiB
TypeScript

import { executeAfwQuery } from './afw-connection'
/**
* AFW Customer data structure
*/
export interface AfwCustomer {
CustId: string
FirmNameCust: string | null
LastName: string | null
FirstName: string | null
Addr1: string | null
Addr2: string | null
City: string | null
State: string | null
ZipCode: string | null
BusPhone: string | null
EMail: string | null
Prod1Code: string | null
ChangedDate: Date | null
}
/**
* AFW Policy data structure (enriched with lookups)
*/
export interface AfwPolicy {
PolId: string
FirmNameCust: string | null
CustNo: number | null
PolNo: string | null
PolEffDate: Date | null
PolExpDate: Date
ParentCompanyName: string | null
WritingCompanyName: string | null
ExecFormattedName: string | null
CsrFormattedName: string | null
Status: string | null
PolTypeLOB: string | null
BillMethod: string | null
BusinessType: string | null
PolicyDepartment: string | null
AddRep1FormattedName: string | null
AddRep2FormattedName: string | null
AddExec1FormattedName: string | null
AddExec2FormattedName: string | null
ANotId: string | null
CustId: string
}
/**
* AFW Employee data structure
*/
export interface AfwEmployee {
EmpCode: string
LastName: string
FirstName: string
MiddleName: string | null
ShortName: string | null
Title: string | null
EMail: string | null
Status: string
IsRep: string | null
IsProd: string | null
EmpSupervisorCode: string | null
DefaultGLDeptCode: string | null
}
/**
* AFW General Ledger Department data structure
*/
export interface AfwGLDepartment {
GLDeptCode: string
Name: string
ShortName: string | null
}
/**
* AFW Company data structure
*/
export interface AfwCompany {
CompanyCode: string
CompanyName: string
Address: string | null
}
/**
* AFW PR Code data structure
*/
export interface AfwPRCode {
PRCode: string
PRName: string
Description: string | null
}
/**
* AFW Policy Contact data structure (deduplicated per customer)
*/
export interface AfwContact {
CustId: string
Name: string
Responsibility: string | null
Title: string | null
AreaCode: string | null
Phone: string | null
MobileAreaCode: string | null
MobilePhone: string | null
EMail: string | null
Notes: string | null
}
/**
* Fetch deduplicated contacts from AFW_PolContact, joined to customers.
* Deduplicates by (CustId, Name, Responsibility) keeping the most recently entered.
*/
export async function fetchAfwContacts(): Promise<AfwContact[]> {
const query = `
WITH ranked AS (
SELECT
c.CustId,
pc.Name,
pc.Responsibility,
pc.Title,
pc.AreaCode,
pc.Phone,
pc.MobileAreaCode,
pc.MobilePhone,
pc.EMail,
pc.Notes,
ROW_NUMBER() OVER (
PARTITION BY c.CustId, pc.Name, pc.Responsibility
ORDER BY pc.EnteredDate DESC
) AS rn
FROM AFW_PolContact pc
INNER JOIN AFW_BasicPolInfo p ON p.PolId = pc.PolId
INNER JOIN AFW_Customer c ON c.CustId = p.CustId
WHERE pc.Status = 'A'
AND pc.Name IS NOT NULL
AND pc.Name != ''
)
SELECT CustId, Name, Responsibility, Title, AreaCode, Phone,
MobileAreaCode, MobilePhone, EMail, Notes
FROM ranked
WHERE rn = 1
ORDER BY CustId, Responsibility, Name
`
return executeAfwQuery<AfwContact>(query)
}
/**
* Fetch customers from AFW database
*/
export async function fetchAfwCustomers(
modifiedSince?: Date
): Promise<AfwCustomer[]> {
let query = `
SELECT
CustId,
FirmNameCust,
LastName,
FirstName,
Addr1,
Addr2,
City,
State,
ZipCode,
BusPhone,
EMail,
Prod1Code,
ChangedDate
FROM AFW_Customer
WHERE Active = 'Y'
`
if (modifiedSince) {
query += ` AND ChangedDate > @modifiedSince`
}
query += ` ORDER BY ChangedDate DESC`
return executeAfwQuery<AfwCustomer>(query, modifiedSince ? { modifiedSince } : undefined)
}
/**
* Fetch policies from AFW database with comprehensive lookups and personnel
* Based on the AFW_PolicyBizVw query structure
*/
export async function fetchAfwPolicies(
startDate?: Date,
endDate?: Date,
modifiedSince?: Date
): Promise<AfwPolicy[]> {
// Default to 2026 policies only
const policyStartDate = startDate || new Date('2026-01-01')
const policyEndDate = endDate || new Date('2026-12-31')
const query = `
WITH addpersonnel AS (
SELECT
PolId,
PolPId,
AFW_Employee.EmpCode,
EmpType,
AFW_Employee.LastName + ', ' + AFW_Employee.FirstName AS Name,
ROW_NUMBER() OVER (
PARTITION BY PolId, EmpType
ORDER BY AFW_PolicyPersonnel.EnteredDate DESC
) AS rn
FROM dbo.AFW_PolicyPersonnel
INNER JOIN AFW_Employee
ON AFW_Employee.EmpCode = AFW_PolicyPersonnel.EmpCode
WHERE IsPrimary = 'N'
)
SELECT
AFW_BasicPolInfo.PolId,
AFW_BasicPolInfo.CustId,
AFW_Customer.FirmNameCust,
AFW_Customer.CustNo,
AFW_BasicPolInfo.PolNo,
AFW_BasicPolInfo.PolEffDate,
AFW_BasicPolInfo.PolExpDate,
ParentCompany.Name AS ParentCompanyName,
WritingCompany.Name AS WritingCompanyName,
ExecCode.LastName + ', ' + ExecCode.FirstName AS ExecFormattedName,
CsrCode.LastName + ', ' + CsrCode.FirstName AS CsrFormattedName,
RenewalRptFlag.Description AS Status,
AFW_BasicPolInfo.PolTypeLOB,
BillMethods.Description AS BillMethod,
TypeOfBusiness.Description AS BusinessType,
AFW_GeneralLedgerDepartment.Name AS PolicyDepartment,
addpersonnel1.Name AS AddRep1FormattedName,
addpersonnel2.Name AS AddRep2FormattedName,
addpersonnel3.Name AS AddExec1FormattedName,
addpersonnel4.Name AS AddExec2FormattedName,
AFW_Customer.ANotId
FROM dbo.AFW_BasicPolInfo
INNER JOIN AFW_Customer
ON AFW_Customer.CustId = AFW_BasicPolInfo.CustId
INNER JOIN AFW_Company AS ParentCompany
ON ParentCompany.CoCode = AFW_BasicPolInfo.CoCode
INNER JOIN AFW_Company AS WritingCompany
ON WritingCompany.CoCode = AFW_BasicPolInfo.WritingCoCode
INNER JOIN AFW_GeneralLedgerDepartment
ON AFW_GeneralLedgerDepartment.GLDeptCode = AFW_BasicPolInfo.GLDeptCode
INNER JOIN AFW_PRCode AS TypeOfBusiness
ON TypeOfBusiness.Code = AFW_BasicPolInfo.TypeOfBus
AND TypeOfBusiness.AttrCode = 'TB'
INNER JOIN AFW_PRCode AS BillMethods
ON BillMethods.Code = AFW_BasicPolInfo.BillMethod
AND BillMethods.AttrCode = 'BM'
INNER JOIN AFW_PRCode AS RenewalRptFlag
ON RenewalRptFlag.Code = AFW_BasicPolInfo.RenewalRptFlag
AND RenewalRptFlag.AttrCode = 'PF'
INNER JOIN AFW_Employee AS ExecCode
ON ExecCode.EmpCode = AFW_BasicPolInfo.ExecCode
INNER JOIN AFW_Employee AS CsrCode
ON CsrCode.EmpCode = AFW_BasicPolInfo.CsrCode
LEFT JOIN addpersonnel AS addpersonnel1
ON addpersonnel1.PolId = AFW_BasicPolInfo.PolId
AND addpersonnel1.rn = 1
AND addpersonnel1.EmpType = 'R'
LEFT JOIN addpersonnel AS addpersonnel2
ON addpersonnel2.PolId = AFW_BasicPolInfo.PolId
AND addpersonnel2.rn = 2
AND addpersonnel2.EmpType = 'R'
LEFT JOIN addpersonnel AS addpersonnel3
ON addpersonnel3.PolId = AFW_BasicPolInfo.PolId
AND addpersonnel3.rn = 1
AND addpersonnel3.EmpType = 'P'
LEFT JOIN addpersonnel AS addpersonnel4
ON addpersonnel4.PolId = AFW_BasicPolInfo.PolId
AND addpersonnel4.rn = 2
AND addpersonnel4.EmpType = 'P'
WHERE
AFW_BasicPolInfo.PolExpDate >= @startDate
AND AFW_BasicPolInfo.PolExpDate <= @endDate
AND AFW_GeneralLedgerDepartment.ShortName IN ('Dept1', 'Dept8', 'Dept9', 'Dept10', 'Dept12')
AND AFW_BasicPolInfo.PolSubType = 'P'
AND AFW_BasicPolInfo.Status != 'D'
ORDER BY AFW_Customer.FirmNameCust
`
const result = await executeAfwQuery<AfwPolicy>(query, {
startDate: policyStartDate,
endDate: policyEndDate,
})
// Log first record to see what we're getting from AFW
if (result.length > 0) {
console.log('📋 Sample AFW Policy Data:', JSON.stringify(result[0], null, 2))
}
console.log(`📊 Total AFW policies fetched: ${result.length}`)
return result
}
/**
* Fetch employees from AFW database
*/
export async function fetchAfwEmployees(): Promise<AfwEmployee[]> {
const query = `
SELECT
EmpCode,
LastName,
FirstName,
MiddleName,
ShortName,
Title,
EMail,
Status,
IsRep,
IsProd,
EmpSupervisorCode,
DefaultGLDeptCode
FROM AFW_Employee
WHERE Status = 'A'
ORDER BY LastName, FirstName
`
return executeAfwQuery<AfwEmployee>(query)
}
/**
* Fetch GL departments from AFW database
*/
export async function fetchAfwGLDepartments(): Promise<AfwGLDepartment[]> {
const query = `
SELECT
GLDeptCode,
Name,
ShortName
FROM AFW_GeneralLedgerDepartment
WHERE Status = 'A'
ORDER BY Name
`
return executeAfwQuery<AfwGLDepartment>(query)
}
/**
* Fetch companies from AFW database
*/
export async function fetchAfwCompanies(): Promise<AfwCompany[]> {
const query = `
SELECT
CompanyCode,
CompanyName,
Address
FROM AFW_Company
ORDER BY CompanyName
`
return executeAfwQuery<AfwCompany>(query)
}
/**
* Fetch PR codes from AFW database
*/
export async function fetchAfwPRCodes(): Promise<AfwPRCode[]> {
const query = `
SELECT
PRCode,
PRName,
Description
FROM AFW_PRCode
ORDER BY PRName
`
return executeAfwQuery<AfwPRCode>(query)
}
/**
* Shape customer data structure (customers with specific ANotId)
*/
export interface AfwShapeCustomer {
CustId: string
CustNo: number
FirmNameCust: string
}
/**
* Fetch customers by ANotId from AFW database
* Generic function to fetch customers with any ANotId
*/
export async function fetchAfwCustomersByAnotId(anotId: string): Promise<AfwShapeCustomer[]> {
const query = `
SELECT
FirmNameCust,
CustNo,
CustId
FROM dbo.AFW_Customer
WHERE ANotId = @anotId
ORDER BY FirmNameCust
`
return executeAfwQuery<AfwShapeCustomer>(query, { anotId })
}
/**
* Fetch Shape program customers from AFW database
* These are customers identified by the specific ANotId for the SHAPE program
* @deprecated Use fetchAfwCustomersByAnotId instead for flexibility
*/
export async function fetchAfwShapeCustomers(): Promise<AfwShapeCustomer[]> {
return fetchAfwCustomersByAnotId('13CF7DCB-F6AF-42C2-A7AB-26641A216A81')
}
/**
* Fetch complete policy business view (similar to AFW_PolicyBizVw)
*/
export interface AfwPolicyBizView {
PolID: string
CustID: string
CustName: string
PolNum: string | null
PolType: string | null
ExpDate: Date
CarrierName: string | null
Premium: number | null
ProducerCode: string | null
GLDeptCode: string | null
GLDeptName: string | null
PrimaryEmpCode: string | null
PrimaryEmpName: string | null
}
export async function fetchAfwPolicyBizView(
expirationMonthsAhead: number = 24
): Promise<AfwPolicyBizView[]> {
const currentDate = new Date()
const futureDate = new Date()
futureDate.setMonth(futureDate.getMonth() + expirationMonthsAhead)
const query = `
SELECT
p.PolID,
p.CustID,
c.CustName,
p.PolNum,
p.PolType,
p.ExpDate,
p.CarrierName,
p.Premium,
c.ProducerCode,
gld.GLDeptCode,
gld.GLDeptName,
pp.EmpCode AS PrimaryEmpCode,
e.EmpName AS PrimaryEmpName
FROM AFW_BasicPolInfo p
INNER JOIN AFW_Customer c ON p.CustID = c.CustID
LEFT JOIN AFW_GeneralLedgerDepartment gld ON c.GLDeptCode = gld.GLDeptCode
LEFT JOIN AFW_PolicyPersonnel pp ON p.PolID = pp.PolID AND pp.IsPrimary = 1
LEFT JOIN AFW_Employee e ON pp.EmpCode = e.EmpCode
WHERE p.ExpDate >= @currentDate
AND p.ExpDate <= @futureDate
AND p.Status = 'Active'
ORDER BY p.ExpDate ASC
`
return executeAfwQuery<AfwPolicyBizView>(query, {
currentDate,
futureDate,
})
}