Skip to content

Commit df8f569

Browse files
author
niffyInsure Dev
committed
feat(backend): add GET /policies and GET /policies/:holder/:policy_id endpoints - Closes #40
1 parent 6daec04 commit df8f569

17 files changed

Lines changed: 1643 additions & 20 deletions

File tree

backend/jest.config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: "ts-jest",
4+
testEnvironment: "node",
5+
roots: ["<rootDir>/tests"],
6+
testMatch: ["**/*.test.ts"],
7+
globals: {
8+
"ts-jest": {
9+
tsconfig: "tsconfig.test.json",
10+
},
11+
},
12+
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Request, Response, NextFunction } from "express";
2+
import {
3+
getPoliciesList,
4+
getPolicySingle,
5+
CursorError,
6+
} from "../services/policy.service";
7+
import { PolicyFilter } from "../repositories/policy.repository";
8+
import { DEFAULT_LIMIT, MAX_LIMIT } from "../helpers/pagination";
9+
10+
/**
11+
* GET /policies
12+
*
13+
* Query params:
14+
* status — "active" | "expired" (omit for all)
15+
* holder — Stellar address string
16+
* after — opaque cursor from previous response
17+
* limit — integer 1–100 (default 20)
18+
*/
19+
export function listPolicies(
20+
req: Request,
21+
res: Response,
22+
next: NextFunction
23+
): void {
24+
try {
25+
const filter: PolicyFilter = {};
26+
27+
const { status, holder, after, limit: limitRaw } = req.query;
28+
29+
if (status !== undefined) {
30+
if (status !== "active" && status !== "expired") {
31+
res.status(400).json({
32+
error: "invalid_filter",
33+
message: `"status" must be "active" or "expired".`,
34+
});
35+
return;
36+
}
37+
filter.status = status;
38+
}
39+
40+
if (typeof holder === "string" && holder.length > 0) {
41+
filter.holder = holder;
42+
}
43+
44+
let limit = DEFAULT_LIMIT;
45+
if (limitRaw !== undefined) {
46+
const parsed = parseInt(String(limitRaw), 10);
47+
if (!Number.isInteger(parsed) || parsed < 1) {
48+
res.status(400).json({
49+
error: "invalid_param",
50+
message: `"limit" must be a positive integer (max ${MAX_LIMIT}).`,
51+
});
52+
return;
53+
}
54+
limit = parsed;
55+
}
56+
57+
const result = getPoliciesList(filter, {
58+
after: typeof after === "string" ? after : undefined,
59+
limit,
60+
});
61+
62+
res.json(result);
63+
} catch (err) {
64+
if (err instanceof CursorError) {
65+
res.status(400).json({ error: "invalid_cursor", message: (err as Error).message });
66+
return;
67+
}
68+
next(err);
69+
}
70+
}
71+
72+
/**
73+
* GET /policies/:holder/:policy_id
74+
*
75+
* Path params:
76+
* holder — URL-encoded Stellar address
77+
* policy_id — per-holder u32 integer
78+
*/
79+
export function getPolicy(
80+
req: Request,
81+
res: Response,
82+
next: NextFunction
83+
): void {
84+
try {
85+
const { holder, policy_id: pidRaw } = req.params;
86+
87+
const policy_id = parseInt(pidRaw, 10);
88+
if (!Number.isInteger(policy_id) || policy_id < 1) {
89+
res.status(400).json({
90+
error: "invalid_param",
91+
message: `"policy_id" must be a positive integer.`,
92+
});
93+
return;
94+
}
95+
96+
const result = getPolicySingle(decodeURIComponent(holder), policy_id);
97+
if (!result) {
98+
res.status(404).json({
99+
error: "not_found",
100+
message: `Policy (holder=${holder}, policy_id=${policy_id}) not found.`,
101+
});
102+
return;
103+
}
104+
105+
res.json(result);
106+
} catch (err) {
107+
next(err);
108+
}
109+
}

backend/src/db/seed.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Seeds the in-memory store with representative dev data.
3+
* Call once at startup in non-production environments.
4+
*/
5+
6+
import { insertClaim, insertPolicy } from "./store";
7+
8+
export function seedDevData(): void {
9+
const p1 = insertPolicy({
10+
holder: "GABC1111111111111111111111111111111111111111111111111111",
11+
policy_id: 1,
12+
policy_type: "Auto",
13+
region: "Medium",
14+
premium: "5000000",
15+
coverage: "500000000",
16+
is_active: true,
17+
start_ledger: 1000,
18+
end_ledger: 9000,
19+
});
20+
21+
const p2 = insertPolicy({
22+
holder: "GABC1111111111111111111111111111111111111111111111111111",
23+
policy_id: 2,
24+
policy_type: "Health",
25+
region: "High",
26+
premium: "8000000",
27+
coverage: "1000000000",
28+
is_active: false,
29+
start_ledger: 500,
30+
end_ledger: 800,
31+
});
32+
33+
const p3 = insertPolicy({
34+
holder: "GXYZ9999999999999999999999999999999999999999999999999999",
35+
policy_id: 1,
36+
policy_type: "Property",
37+
region: "Low",
38+
premium: "3000000",
39+
coverage: "200000000",
40+
is_active: true,
41+
start_ledger: 2000,
42+
end_ledger: 12000,
43+
});
44+
45+
insertClaim({
46+
claim_id: 1,
47+
policy_id: p2.policy_id,
48+
claimant: p2.holder,
49+
amount: "100000000",
50+
details: "Hospital visit after accident",
51+
image_urls: ["ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"],
52+
status: "Approved",
53+
approve_votes: 5,
54+
reject_votes: 1,
55+
});
56+
57+
insertClaim({
58+
claim_id: 2,
59+
policy_id: p1.policy_id,
60+
claimant: p1.holder,
61+
amount: "50000000",
62+
details: "Rear-end collision repair",
63+
image_urls: [],
64+
status: "Processing",
65+
approve_votes: 2,
66+
reject_votes: 0,
67+
});
68+
69+
insertClaim({
70+
claim_id: 3,
71+
policy_id: p3.policy_id,
72+
claimant: p3.holder,
73+
amount: "20000000",
74+
details: "Storm damage to roof",
75+
image_urls: [],
76+
status: "Rejected",
77+
approve_votes: 1,
78+
reject_votes: 4,
79+
});
80+
}

backend/src/db/store.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* In-memory data store with indexed maps for O(1) lookups and
3+
* sorted insertion order for stable cursor-based pagination.
4+
*
5+
* Indexes maintained:
6+
* - byGlobalSeq: Map<global_seq, Policy> — primary cursor index
7+
* - byHolder: Map<holder, Policy[]> — filter by holder
8+
* - byComposite: Map<"holder:policy_id", Policy> — point lookup
9+
*
10+
* All writes go through `insertPolicy` / `upsertPolicy` to keep indexes
11+
* consistent. Concurrent inserts are safe because JS is single-threaded.
12+
*/
13+
14+
import { Claim, Policy } from "../types/policy";
15+
16+
let _seq = 0;
17+
18+
const byGlobalSeq = new Map<number, Policy>();
19+
const byHolder = new Map<string, Policy[]>();
20+
const byComposite = new Map<string, Policy>();
21+
22+
const claims = new Map<number, Claim>();
23+
/** claim_id → policy composite key, for join without N+1 */
24+
const claimsByComposite = new Map<string, Claim[]>();
25+
26+
function compositeKey(holder: string, policy_id: number): string {
27+
return `${holder}:${policy_id}`;
28+
}
29+
30+
export function insertPolicy(p: Omit<Policy, "global_seq">): Policy {
31+
_seq += 1;
32+
const record: Policy = { ...p, global_seq: _seq };
33+
const ck = compositeKey(p.holder, p.policy_id);
34+
35+
byGlobalSeq.set(_seq, record);
36+
byComposite.set(ck, record);
37+
38+
const holderList = byHolder.get(p.holder) ?? [];
39+
holderList.push(record);
40+
byHolder.set(p.holder, holderList);
41+
42+
return record;
43+
}
44+
45+
export function upsertPolicy(p: Omit<Policy, "global_seq">): Policy {
46+
const ck = compositeKey(p.holder, p.policy_id);
47+
const existing = byComposite.get(ck);
48+
if (existing) {
49+
// Update in-place, preserving global_seq for stable pagination
50+
Object.assign(existing, p);
51+
return existing;
52+
}
53+
return insertPolicy(p);
54+
}
55+
56+
export function getPolicyByComposite(
57+
holder: string,
58+
policy_id: number
59+
): Policy | undefined {
60+
return byComposite.get(compositeKey(holder, policy_id));
61+
}
62+
63+
export function getPolicyBySeq(seq: number): Policy | undefined {
64+
return byGlobalSeq.get(seq);
65+
}
66+
67+
/** Returns all policies in ascending global_seq order. */
68+
export function allPoliciesOrdered(): Policy[] {
69+
return Array.from(byGlobalSeq.values());
70+
}
71+
72+
export function getPoliciesByHolder(holder: string): Policy[] {
73+
return byHolder.get(holder) ?? [];
74+
}
75+
76+
// ── Claims ───────────────────────────────────────────────────────────────────
77+
78+
export function insertClaim(c: Claim): void {
79+
claims.set(c.claim_id, c);
80+
const ck = compositeKey(c.claimant, c.policy_id);
81+
const list = claimsByComposite.get(ck) ?? [];
82+
list.push(c);
83+
claimsByComposite.set(ck, list);
84+
}
85+
86+
export function getClaimsByPolicy(holder: string, policy_id: number): Claim[] {
87+
return claimsByComposite.get(compositeKey(holder, policy_id)) ?? [];
88+
}
89+
90+
/** Bulk-fetch claims for a list of (holder, policy_id) pairs — avoids N+1. */
91+
export function getClaimsBatch(
92+
keys: Array<{ holder: string; policy_id: number }>
93+
): Map<string, Claim[]> {
94+
const result = new Map<string, Claim[]>();
95+
for (const { holder, policy_id } of keys) {
96+
const ck = compositeKey(holder, policy_id);
97+
result.set(ck, claimsByComposite.get(ck) ?? []);
98+
}
99+
return result;
100+
}
101+
102+
/** Reset store — used in tests only. */
103+
export function _resetStore(): void {
104+
_seq = 0;
105+
byGlobalSeq.clear();
106+
byHolder.clear();
107+
byComposite.clear();
108+
claims.clear();
109+
claimsByComposite.clear();
110+
}

0 commit comments

Comments
 (0)