sh-mcp/packages/payments/src/client.ts

123 lines
4.2 KiB
TypeScript
Raw Normal View History

Phase 0b slice: monorepo scaffold + @sh-mcp/shared core + integration packages (#2) * Phase 0b slice: monorepo scaffold + shared core + integration packages The 0a-INDEPENDENT code slice (one-shot via af-0b-package-slice workflow: Haiku scaffold + Sonnet packages, Sonnet fix-to-green). Nothing deploys; no CDK/servers. - Monorepo scaffold: npm workspaces, strict TS (NodeNext), vitest (80% gate), eslint 9 flat config, prettier; ci.yaml/deploy.yaml callers (Node 24, enable-qemu). - @sh-mcp/shared: transport-agnostic core — Scope/AuthContext/ToolDef, ToolRegistry, redact()+maskValue() (PII), OpenAPI 3.1 generator. AUTH STUBBED behind an AuthProvider interface (TODO auth-layer-0a); JWT/aud/client_id/JWKS/deny-list deferred per design.md §2. - 9 integration packages (qbo, google-maps, internal-data, payments, knowledge-base, gmail, calendar, tasks, reminders): tools against shared, external deps mocked behind injected client interfaces; finance handlers call redact(). Verified green: tsc -b clean, vitest 245/245, eslint 0 errors. Auth mechanism intentionally deferred until the 0a spike resolves it (G16/§0.4). * Complete Cognito auth provider + Phase 1 build brief Finish the WIP CognitoAuthProvider (client_id allow-list as audience boundary, finance TTL ceiling, deny-list, scope-prefix stripping) with its test suite, and check in docs/build-plan-phase-1.md so the Phase 1 work has its governing brief in-tree (design.md §2.5). * ci: disable cdk synth for Phase 0b (no CDK app yet) The reusable ci-typescript-cdk workflow defaults run-cdk-synth: true, but the Phase 0b package scaffold has no cdk.json or stacks, so cdk synth fails with '--app is required'. Disable it here; Phase 1 re-enables it with the server CDK stubs.
2026-06-26 12:42:17 -04:00
/**
* PaymentsClient interface + stub implementation.
*
* The real implementation would use the AWS SDK DynamoDB DocumentClient
* targeting the PaymentsDashboard table. That call is clearly marked below
* and guarded behind the interface so tests can inject a mock without any
* AWS credentials or network access at import time.
*
* TODO(auth-layer): when the real DynamoDB client is wired, pull the table
* name and region from environment variables set by the CDK stack rather than
* hard-coding them here. Credentials must come from the Lambda execution
* role (no explicit key/secret in code or Secrets Manager for IAM-auth calls).
*/
export interface Payment {
paymentId: string;
vendor: string;
vendorContact?: string;
amount: number;
currency: string;
invoiceNumber?: string;
checkNumber?: string;
/** ISO-8601 date string */
paymentDate: string;
status: "pending" | "cleared" | "voided" | "failed";
/** Bank account number — MUST be redacted before leaving the server */
bankAccountNumber?: string;
/** Bank routing number — MUST be redacted before leaving the server */
bankRoutingNumber?: string;
/** Card number (last-four or full) — MUST be redacted before leaving the server */
cardNumber?: string;
/** ACH or wire memo */
memo?: string;
}
export interface PaymentsQueryOptions {
limit?: number;
}
/**
* The contract every PaymentsDashboard client must satisfy.
* Tests inject a MockPaymentsClient; production injects DynamoPaymentsClient.
*/
export interface PaymentsClient {
/** Return all payments for a given vendor name (case-insensitive prefix match). */
getByVendor(
vendor: string,
opts?: PaymentsQueryOptions,
): Promise<Payment[]>;
/** Return the payment(s) matching an invoice number. */
getByInvoice(
invoiceNumber: string,
opts?: PaymentsQueryOptions,
): Promise<Payment[]>;
/** Return the payment matching a check number. */
getByCheck(
checkNumber: string,
opts?: PaymentsQueryOptions,
): Promise<Payment[]>;
}
// ---------------------------------------------------------------------------
// Stub production implementation
// ---------------------------------------------------------------------------
/**
* Thin wrapper around the DynamoDB PaymentsDashboard table.
*
* STUBBED: the actual DynamoDB calls are replaced with a thrown error so that
* this file is safe to import in any environment without AWS credentials. To
* activate the real implementation:
* 1. npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
* 2. Replace each `throw new Error("STUB")` block with the real query.
*
* TODO(real-impl): implement DynamoDB GSI queries:
* - VendorIndex (pk = vendor_normalized)
* - InvoiceIndex (pk = invoiceNumber)
* - CheckIndex (pk = checkNumber)
*/
export class DynamoPaymentsClient implements PaymentsClient {
private readonly tableName: string;
constructor(tableName = process.env["PAYMENTS_TABLE"] ?? "PaymentsDashboard") {
this.tableName = tableName;
// The DynamoDB DocumentClient is intentionally NOT instantiated here to
// avoid any AWS SDK import side-effects at module load time. Instantiate
// it lazily inside each method once the real implementation is added.
void this.tableName; // suppress unused-var lint until real impl lands
}
async getByVendor(
_vendor: string,
_opts?: PaymentsQueryOptions,
): Promise<Payment[]> {
// TODO(real-impl): query VendorIndex GSI with vendor_normalized = vendor.toLowerCase()
throw new Error(
"DynamoPaymentsClient is a stub — inject a real or mock client instead.",
);
}
async getByInvoice(
_invoiceNumber: string,
_opts?: PaymentsQueryOptions,
): Promise<Payment[]> {
// TODO(real-impl): query InvoiceIndex GSI with invoiceNumber = invoiceNumber
throw new Error(
"DynamoPaymentsClient is a stub — inject a real or mock client instead.",
);
}
async getByCheck(
_checkNumber: string,
_opts?: PaymentsQueryOptions,
): Promise<Payment[]> {
// TODO(real-impl): query CheckIndex GSI with checkNumber = checkNumber
throw new Error(
"DynamoPaymentsClient is a stub — inject a real or mock client instead.",
);
}
}