/** * 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; /** Return the payment(s) matching an invoice number. */ getByInvoice(invoiceNumber: string, opts?: PaymentsQueryOptions): Promise; /** Return the payment matching a check number. */ getByCheck(checkNumber: string, opts?: PaymentsQueryOptions): Promise; } // --------------------------------------------------------------------------- // 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 { // 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 { // 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 { // TODO(real-impl): query CheckIndex GSI with checkNumber = checkNumber throw new Error('DynamoPaymentsClient is a stub — inject a real or mock client instead.'); } }