From 0dede0d90a85e2d57ed32e09bb6cc374781e4257 Mon Sep 17 00:00:00 2001 From: berkansr Date: Mon, 22 Jun 2026 14:48:21 +0300 Subject: [PATCH] feat: align policy sdk with core endpoints --- packages/contracts/scripts/generate-meta.ts | 15 +- packages/contracts/src/policies.ts | 159 +++++++++++++- packages/sdk/src/clients/policy.ts | 55 ++++- packages/sdk/src/core/endpoints.ts | 31 ++- packages/sdk/test/integration/policy.spec.ts | 208 +++++++++++++++++++ packages/sdk/test/unit/client.spec.ts | 4 + 6 files changed, 451 insertions(+), 21 deletions(-) diff --git a/packages/contracts/scripts/generate-meta.ts b/packages/contracts/scripts/generate-meta.ts index a6eea5b..794ce56 100644 --- a/packages/contracts/scripts/generate-meta.ts +++ b/packages/contracts/scripts/generate-meta.ts @@ -147,6 +147,15 @@ async function writeFormatted(path: string, content: string): Promise { writeFileSync(path, formatted, 'utf-8'); } +function toGlobPath(path: string): string { + return path.replace(/\\/g, '/'); +} + +function isWithinDir(path: string, dir: string): boolean { + const normalizedPath = toGlobPath(path); + const normalizedDir = toGlobPath(dir).replace(/\/$/, ''); + return normalizedPath === normalizedDir || normalizedPath.startsWith(`${normalizedDir}/`); +} // Parse CLI args: first = primary dir, rest = extra dirs const args = process.argv.slice(2).map((d) => resolve(ROOT, d)); if (args.length === 0) { @@ -169,7 +178,7 @@ const project = new Project({ // Add extra directory source files to the project so ts-morph can resolve them for (const dir of extraDirs) { - project.addSourceFilesAtPaths(join(dir, '*.ts')); + project.addSourceFilesAtPaths(toGlobPath(join(dir, '*.ts'))); } interface FieldEntry { @@ -224,7 +233,7 @@ const fileMap = new Map(); const scanDirs = [primaryDir, ...extraDirs]; for (const scanDir of scanDirs) { - for (const sourceFile of project.getSourceFiles(join(scanDir, '*.ts'))) { + for (const sourceFile of project.getSourceFiles(toGlobPath(join(scanDir, '*.ts')))) { const filePath = sourceFile.getFilePath(); // Skip already-generated files and the meta-types file @@ -401,7 +410,7 @@ for (const [filePath, metas] of fileMap) { const lines: string[] = [HEADER, `import type { ModelMeta } from "${importPath}";\n`]; - const isPrimaryDir = filePath.startsWith(primaryDir); + const isPrimaryDir = isWithinDir(filePath, primaryDir); for (const meta of metas) { const varName = `${meta.interfaceName}Meta`; diff --git a/packages/contracts/src/policies.ts b/packages/contracts/src/policies.ts index 078c752..9cc9c5f 100644 --- a/packages/contracts/src/policies.ts +++ b/packages/contracts/src/policies.ts @@ -12,10 +12,12 @@ import type { Channel, UserReference, Coverage, + AssetType, ProposalSnapshotVehicle, ProposalSnapshotProperty, } from './common.js'; import type { CustomerPhoneNumber } from './common.js'; +import type { PolicyVersionType } from './graphql/policies.js'; import type { ProposalSnapshotCustomer } from './proposals.js'; // ============================================================================ @@ -156,10 +158,35 @@ type PolicySnapshotElectronicDevice = Record; export interface GetPolicyDetailRequest { /** * The unique identifier of the insurance policy for which detailed information is requested. - * - * Ayrıntılı bilgisi istenen sigorta poliçesinin benzersiz tanımlayıcısı. */ readonly policyId: string; + + /** Optional policy version to retrieve. When omitted, the latest version is returned. */ + readonly version?: number; +} + +/** + * Policy version source. + */ +export enum PolicyVersionSource { + Unknown = 'UNKNOWN', + AgentPanel = 'AGENT_PANEL', + PolicyTransfer = 'POLICY_TRANSFER', + Manual = 'MANUAL', +} + +/** + * Summary item for a version stored on a policy. + */ +export interface PolicyVersionSummary { + readonly version: number; + readonly versionType: PolicyVersionType; + readonly timestamp: string; + readonly source: PolicyVersionSource; + readonly updateReasonText: string | null; + readonly hasSnapshotChange: boolean; + readonly hasCoverageChange: boolean; + readonly hasPremiumChange: boolean; } /** @@ -210,7 +237,7 @@ export interface GetPolicyDetailResult { * * Sigorta ürününün tanımlayıcısı. */ - readonly productId: number; + readonly productId: number | null; /** * The identifier of the insurance company providing the coverage. @@ -238,14 +265,14 @@ export interface GetPolicyDetailResult { * * Vergi ve ücretler öncesi net prim tutarı. */ - readonly netPremium: number; + readonly netPremium: number | null; /** * The total premium amount including all taxes and fees. * * Tüm vergi ve ücretler dahil toplam prim tutarı. */ - readonly grossPremium: number; + readonly grossPremium: number | null; /** * The commission amount paid to the agent, if applicable. @@ -268,6 +295,18 @@ export interface GetPolicyDetailResult { */ readonly currency: Currency; + /** Exchange rate used to convert policy monetary amounts to Turkish Lira. */ + readonly exchangeRate: number; + + /** Net premium converted to Turkish Lira. */ + readonly netPremiumTL: number | null; + + /** Gross premium converted to Turkish Lira. */ + readonly grossPremiumTL: number | null; + + /** Commission converted to Turkish Lira. */ + readonly commissionTL: number | null; + /** * The proposal number assigned by the insurance company. * @@ -287,7 +326,7 @@ export interface GetPolicyDetailResult { * * Harici sigorta hizmetlerindeki poliçe için referans tanımlayıcısı. */ - readonly insuranceServicesPolicyReference: string; + readonly insuranceServicesPolicyReference: string | null; /** * The date and time when the policy was created. @@ -379,6 +418,27 @@ export interface GetPolicyDetailResult { * Bu poliçenin satıldığı satış kanalı. */ readonly channel: Channel; + + /** Additional policy metadata. */ + readonly metadata: Readonly> | null; + + /** Branch assigned to the policy. */ + readonly agentBranchId: string | null; + + /** Net premium difference introduced by the current version. */ + readonly netPremiumChange: number | null; + + /** Gross premium difference introduced by the current version. */ + readonly grossPremiumChange: number | null; + + /** Commission difference introduced by the current version. */ + readonly commissionChange: number | null; + + /** Current policy version number. */ + readonly currentVersion: number; + + /** Available policy versions. */ + readonly versions: readonly PolicyVersionSummary[] | null; } // ============================================================================ @@ -502,6 +562,53 @@ export interface SetPolicyBranchRequest { readonly branchId: string; } +// ============================================================================ +// POLICY CLAIM TYPES +// ============================================================================ + +/** + * Request to search for an existing policy that can be claimed by the current agent user. + */ +export interface SearchPolicyForClaimRequest { + readonly insuranceCompanyId: number; + readonly policyNumber: string; + readonly endorsementNumber: number; + readonly renewalNumber: number; +} + +/** + * Response for policy claim search. + */ +export interface SearchPolicyForClaimResult { + readonly found: boolean; + readonly owned: boolean; + readonly ownedBySelf: boolean; + readonly policyId: string | null; + readonly requiresBranchSelection: boolean; + readonly candidateBranchIds: readonly string[]; + readonly endorsementAhead: boolean; + readonly lastEndorsementNumber: number | null; +} + +/** + * Request to claim ownership of an unowned policy. + */ +export interface ClaimPolicyRequest { + readonly policyId: string; + readonly agentBranchId?: string | null; +} + +/** + * Request to append a manual endorsement to an existing policy. + */ +export interface AddPolicyEndorsementRequest { + readonly policyId: string; + readonly netPremium?: number | null; + readonly grossPremium?: number | null; + readonly commission?: number | null; + readonly reason: string; +} + // ============================================================================ // MANUAL POLICY TYPES // ============================================================================ @@ -510,13 +617,29 @@ export interface SetPolicyBranchRequest { * Request to create manual policy */ export interface CreateManualPolicyRequest { - readonly customerId: string; + readonly policyNumber: string; readonly insuranceCompanyId: number; + readonly productId?: number | null; readonly productBranch: ProductBranch; - readonly policyNumber: string; + readonly insuredCustomerId: string; + readonly insurerCustomerId: string; + readonly coverage?: Coverage | null; readonly startDate: string; readonly endDate: string; - readonly premium: number; + readonly arrangementDate?: string | null; + readonly netPremium?: number | null; + readonly grossPremium?: number | null; + readonly commission?: number | null; + readonly renewalNumber: number; + readonly daskPolicyNumber?: string | null; + readonly proposalId?: string | null; + readonly currency?: Currency; + readonly exchangeRate?: number; + readonly paymentType?: PaymentOption; + readonly assetId?: string | null; + readonly assetType?: AssetType | null; + readonly metadata?: Readonly> | null; + readonly agentBranchId?: string | null; } /** @@ -532,9 +655,25 @@ export interface CreateManualPolicyResult { export interface UpdateManualPolicyRequest { readonly policyId: string; readonly policyNumber?: string; + readonly insuranceCompanyId?: number | null; + readonly productId?: number | null; + readonly productBranch?: ProductBranch | null; + readonly insuredCustomerId?: string | null; + readonly insurerCustomerId?: string | null; + readonly coverage?: Coverage | null; readonly startDate?: string; readonly endDate?: string; - readonly premium?: number; + readonly arrangementDate?: string | null; + readonly netPremium?: number | null; + readonly grossPremium?: number | null; + readonly commission?: number | null; + readonly daskPolicyNumber?: string | null; + readonly proposalId?: string | null; + readonly currency?: Currency | null; + readonly exchangeRate?: number | null; + readonly paymentType?: PaymentOption | null; + readonly assetId?: string | null; + readonly assetType?: AssetType | null; } // ============================================================================ diff --git a/packages/sdk/src/clients/policy.ts b/packages/sdk/src/clients/policy.ts index e6dac75..f9060d0 100644 --- a/packages/sdk/src/clients/policy.ts +++ b/packages/sdk/src/clients/policy.ts @@ -19,6 +19,10 @@ import type { CreateManualPolicyRequest, CreateManualPolicyResult, UpdateManualPolicyRequest, + SearchPolicyForClaimRequest, + SearchPolicyForClaimResult, + ClaimPolicyRequest, + AddPolicyEndorsementRequest, GetPolicyCountAndPremiumAnalyticsRequest, GetPolicyCountAndPremiumAnalyticsResult, GetPolicyRenewalAnalyticsRequest, @@ -95,7 +99,7 @@ export class InsurUpPolicyClient { options?: RequestOptions ): Promise> { return this.http.get( - endpoints.policies.getPolicyDetail.render(request.policyId), + endpoints.policies.getPolicyDetail.render(request.policyId, request.version), options ); } @@ -168,7 +172,7 @@ export class InsurUpPolicyClient { request: SetPolicyBranchRequest, options?: RequestOptions ): Promise { - return this.http.postNoContent( + return this.http.putNoContent( endpoints.policies.setPolicyBranch.render(request.policyId), request, options @@ -212,6 +216,53 @@ export class InsurUpPolicyClient { options ); } + /** + * Searches for an existing unowned policy that the current agent user can claim. + * + * @param request Policy claim search criteria + * @returns Policy claim search result + */ + async searchPolicyForClaim( + request: SearchPolicyForClaimRequest, + options?: RequestOptions + ): Promise> { + return this.http.post( + endpoints.policies.searchPolicyForClaim.definition, + request, + options + ); + } + + /** + * Assigns an unowned policy to the current agent user and optionally to a selected branch. + * + * @param request Policy claim request + * @returns Operation result + */ + async claimPolicy(request: ClaimPolicyRequest, options?: RequestOptions): Promise { + return this.http.postNoContent( + endpoints.policies.claimPolicy.render(request.policyId), + request, + options + ); + } + + /** + * Appends a manual endorsement as the next version of an existing policy. + * + * @param request Manual endorsement request + * @returns Operation result + */ + async addPolicyEndorsement( + request: AddPolicyEndorsementRequest, + options?: RequestOptions + ): Promise { + return this.http.postNoContent( + endpoints.policies.addPolicyEndorsement.render(request.policyId), + request, + options + ); + } /** * Retrieves comprehensive policy count and gross premium analytics with flexible filtering and grouping options. diff --git a/packages/sdk/src/core/endpoints.ts b/packages/sdk/src/core/endpoints.ts index e2ad9e8..c426756 100644 --- a/packages/sdk/src/core/endpoints.ts +++ b/packages/sdk/src/core/endpoints.ts @@ -743,8 +743,12 @@ export const proposals = { export const policies = { getPolicyDetail: { definition: 'policies/{PolicyId}', - render: (policyId: string): string => - 'policies/{PolicyId}'.replace('{PolicyId}', encodeURIComponent(policyId)), + render: (policyId: string, version?: number): string => { + const base = 'policies/{PolicyId}'.replace('{PolicyId}', encodeURIComponent(policyId)); + return version !== undefined + ? base + '?version=' + encodeURIComponent(version.toString()) + : base; + }, }, fetchPolicyDocument: { @@ -780,21 +784,36 @@ export const policies = { render: (policyId: string): string => 'policies/manual/{PolicyId}'.replace('{PolicyId}', encodeURIComponent(policyId)), }, + searchPolicyForClaim: { + definition: 'policies/search-for-claim', + }, + + claimPolicy: { + definition: 'policies/{PolicyId}/claim', + render: (policyId: string): string => + 'policies/{PolicyId}/claim'.replace('{PolicyId}', encodeURIComponent(policyId)), + }, + + addPolicyEndorsement: { + definition: 'policies/{PolicyId}/endorsements', + render: (policyId: string): string => + 'policies/{PolicyId}/endorsements'.replace('{PolicyId}', encodeURIComponent(policyId)), + }, getPolicyCountAndPremiumAnalytics: { - definition: 'policies/analytics/count-and-premium', + definition: 'policies/count-and-premium-analytics', }, getPolicyRenewalAnalytics: { - definition: 'policies/analytics/renewal', + definition: 'policies/renewal-analytics', }, getPolicyDistributionByBranch: { - definition: 'policies/analytics/distribution-by-branch', + definition: 'policies/distribution-by-branch-analytics', }, getRepresentativeEarningsAnalytics: { - definition: 'policies/analytics/representative-earnings', + definition: 'policies/representative-earnings-analytics', }, } as const; diff --git a/packages/sdk/test/integration/policy.spec.ts b/packages/sdk/test/integration/policy.spec.ts index 212d13c..538283a 100644 --- a/packages/sdk/test/integration/policy.spec.ts +++ b/packages/sdk/test/integration/policy.spec.ts @@ -4,6 +4,7 @@ import { InsurUpServerErrorType } from '../../src/core/result'; import { BASE_URL, server } from './server'; import { notFound, setupIntegrationTest } from './setup'; import { samplePolicyDetail, samplePolicyDocument } from './fixtures/policy'; +import { AssetType, Currency, PaymentOption, ProductBranch } from '@insurup/contracts'; const t = setupIntegrationTest(); @@ -53,4 +54,211 @@ describe('PolicyClient', () => { expect(result.data.url).toBe(samplePolicyDocument.url); } }); + + it('getPolicyDetail includes version query when provided', async () => { + server.use( + http.get(`${BASE_URL}/policies/:id`, ({ params, request }) => { + expect(params.id).toBe(samplePolicyDetail.id); + expect(new URL(request.url).searchParams.get('version')).toBe('2'); + return HttpResponse.json({ ...samplePolicyDetail, currentVersion: 2, versions: [] }); + }) + ); + + const result = await t.client.policies.getPolicyDetail({ + policyId: samplePolicyDetail.id, + version: 2, + }); + + expect(result.kind).toBe('success'); + if (result.kind === 'success') { + expect(result.data.currentVersion).toBe(2); + } + }); + + it('searchPolicyForClaim posts composite policy key', async () => { + server.use( + http.post(`${BASE_URL}/policies/search-for-claim`, async ({ request }) => { + const body = (await request.json()) as { + insuranceCompanyId: number; + policyNumber: string; + endorsementNumber: number; + renewalNumber: number; + }; + expect(body).toEqual({ + insuranceCompanyId: 42, + policyNumber: 'P-123', + endorsementNumber: 1, + renewalNumber: 0, + }); + return HttpResponse.json({ + found: true, + owned: false, + ownedBySelf: false, + policyId: 'POL-CLAIM', + requiresBranchSelection: true, + candidateBranchIds: ['BR-1', 'BR-2'], + endorsementAhead: false, + lastEndorsementNumber: null, + }); + }) + ); + + const result = await t.client.policies.searchPolicyForClaim({ + insuranceCompanyId: 42, + policyNumber: 'P-123', + endorsementNumber: 1, + renewalNumber: 0, + }); + + expect(result.kind).toBe('success'); + if (result.kind === 'success') { + expect(result.data.policyId).toBe('POL-CLAIM'); + expect(result.data.candidateBranchIds).toEqual(['BR-1', 'BR-2']); + } + }); + + it('claimPolicy posts to policy claim endpoint', async () => { + server.use( + http.post(`${BASE_URL}/policies/:id/claim`, async ({ params, request }) => { + expect(params.id).toBe('POL-CLAIM'); + const body = (await request.json()) as { policyId: string; agentBranchId: string }; + expect(body).toEqual({ policyId: 'POL-CLAIM', agentBranchId: 'BR-1' }); + return new HttpResponse(null, { status: 204 }); + }) + ); + + const result = await t.client.policies.claimPolicy({ + policyId: 'POL-CLAIM', + agentBranchId: 'BR-1', + }); + + expect(result.kind).toBe('success'); + }); + + it('addPolicyEndorsement posts manual endorsement differences', async () => { + server.use( + http.post(`${BASE_URL}/policies/:id/endorsements`, async ({ params, request }) => { + expect(params.id).toBe('POL-1'); + const body = (await request.json()) as { + policyId: string; + netPremium: number; + grossPremium: number; + commission: number; + reason: string; + }; + expect(body).toEqual({ + policyId: 'POL-1', + netPremium: -10, + grossPremium: -12, + commission: -1, + reason: 'Correction', + }); + return new HttpResponse(null, { status: 204 }); + }) + ); + + const result = await t.client.policies.addPolicyEndorsement({ + policyId: 'POL-1', + netPremium: -10, + grossPremium: -12, + commission: -1, + reason: 'Correction', + }); + + expect(result.kind).toBe('success'); + }); + + it('createManualPolicy accepts the expanded core request shape', async () => { + server.use( + http.post(`${BASE_URL}/policies/manual`, async ({ request }) => { + const body = (await request.json()) as { insuredCustomerId: string; agentBranchId: string }; + expect(body.insuredCustomerId).toBe('CUS-INSURED'); + expect(body.agentBranchId).toBe('BR-1'); + return HttpResponse.json({ policyId: 'POL-MANUAL' }, { status: 201 }); + }) + ); + + const result = await t.client.policies.createManualPolicy({ + policyNumber: 'M-1', + insuranceCompanyId: 42, + productId: 7, + productBranch: ProductBranch.Kasko, + insuredCustomerId: 'CUS-INSURED', + insurerCustomerId: 'CUS-INSURER', + coverage: null, + startDate: '2026-01-01', + endDate: '2027-01-01', + arrangementDate: '2026-01-01', + netPremium: 100, + grossPremium: 120, + commission: 10, + renewalNumber: 0, + daskPolicyNumber: null, + proposalId: null, + currency: Currency.TurkishLira, + exchangeRate: 1, + paymentType: PaymentOption.SyncCreditCard, + assetId: 'ASSET-1', + assetType: AssetType.Vehicle, + metadata: { source: 'agent-panel' }, + agentBranchId: 'BR-1', + }); + + expect(result.kind).toBe('success'); + if (result.kind === 'success') { + expect(result.data.policyId).toBe('POL-MANUAL'); + } + }); + + it('updateManualPolicy accepts expanded partial fields', async () => { + server.use( + http.put(`${BASE_URL}/policies/manual/:id`, async ({ params, request }) => { + expect(params.id).toBe('POL-MANUAL'); + const body = (await request.json()) as { policyId: string; exchangeRate: number }; + expect(body).toMatchObject({ policyId: 'POL-MANUAL', exchangeRate: 2 }); + return new HttpResponse(null, { status: 204 }); + }) + ); + + const result = await t.client.policies.updateManualPolicy({ + policyId: 'POL-MANUAL', + policyNumber: 'M-2', + insuranceCompanyId: 42, + productId: 8, + productBranch: ProductBranch.Konut, + insuredCustomerId: 'CUS-INSURED-2', + insurerCustomerId: 'CUS-INSURER-2', + coverage: null, + startDate: '2026-02-01', + endDate: '2027-02-01', + arrangementDate: '2026-02-01', + netPremium: 200, + grossPremium: 240, + commission: 20, + daskPolicyNumber: 'DASK-1', + proposalId: 'PROP-1', + currency: Currency.Euro, + exchangeRate: 2, + paymentType: PaymentOption.SyncOpenAccount, + assetId: 'ASSET-2', + assetType: AssetType.Property, + }); + + expect(result.kind).toBe('success'); + }); + + it('setPolicyBranch uses PUT according to the core endpoint contract', async () => { + server.use( + http.put(`${BASE_URL}/policies/:id/branch`, async ({ params, request }) => { + expect(params.id).toBe('POL-1'); + const body = (await request.json()) as { policyId: string; branchId: string }; + expect(body).toEqual({ policyId: 'POL-1', branchId: 'BR-1' }); + return new HttpResponse(null, { status: 204 }); + }) + ); + + const result = await t.client.policies.setPolicyBranch({ policyId: 'POL-1', branchId: 'BR-1' }); + + expect(result.kind).toBe('success'); + }); }); diff --git a/packages/sdk/test/unit/client.spec.ts b/packages/sdk/test/unit/client.spec.ts index dfb8afa..2bf4773 100644 --- a/packages/sdk/test/unit/client.spec.ts +++ b/packages/sdk/test/unit/client.spec.ts @@ -142,6 +142,10 @@ describe('DefaultInsurUpClient', () => { 'fetchPolicyDocument', 'sendPolicyDocumentToCustomer', 'setPolicyRepresentative', + 'setPolicyBranch', + 'searchPolicyForClaim', + 'claimPolicy', + 'addPolicyEndorsement', ], cases: [ 'createCancelCase',