# LedgerFlow Backend Node.js serverless backend for the LedgerFlow B2B accounting app. Runs entirely on AWS Lambda + API Gateway with Google SSO authentication. --- ## Architecture ``` Browser (Google Sign-In) │ │ Bearer: Google ID Token ▼ API Gateway (HTTP API) │ ├── Lambda Authorizer ──── verifies Google JWT ──── DynamoDB (sessions) │ ├── POST /auth/me ← public, verifies token, returns user ├── GET /auth/config ← public, returns Google Client ID │ ├── /pos/** ← Purchase Orders Lambda │ ├── GET /pos list (paginated) │ ├── POST /pos create │ ├── GET /pos/:id get one │ ├── PUT /pos/:id update │ ├── DELETE /pos/:id delete │ ├── POST /pos/import bulk import │ └── GET /pos/dynamo-scan proxy scan of customer's DynamoDB table │ ├── /invoices/** ← Invoices Lambda │ ├── GET /invoices list │ ├── POST /invoices create (bills against PO) │ ├── GET /invoices/:id get one │ ├── PUT /invoices/:id update (full edit with line items; marks modified_after_send if already EDI-submitted) │ ├── PATCH /invoices/:id/status status transition │ └── DELETE /invoices/:id delete (any non-paid invoice; reverses PO billed amount) │ ├── /settings/** ← Settings Lambda │ ├── GET /settings get user settings │ └── PUT /settings update settings │ └── /edi/** ← EDI Lambda ├── POST /edi/submit submit to AWS B2B Data Interchange ├── POST /edi/preview preview X12 document (no submission) ├── GET /edi/transactions transaction history ├── GET /edi/transactions/:id one transaction └── GET /edi/status/:isaControl poll for 997 ACK DynamoDB Tables: ledgerflow-pos Purchase Orders (app-managed) ledgerflow-invoices Invoices ledgerflow-edi-transactions EDI transaction log ledgerflow-sessions Google auth sessions (TTL 7 days) ledgerflow-settings User/company settings External Tables: purchase-orders Coupa POs (read-only, ship_to address for EDI) S3 Buckets (EDI file exchange): ledgerflow-edi-input-{account} ledgerflow-edi-output-{account} ``` --- ## Prerequisites - Node.js 20+ - AWS CLI configured (`aws configure`) - AWS CDK bootstrapped (`npx cdk bootstrap`) - Google Cloud project with OAuth 2.0 credentials - AWS B2B Data Interchange configured (optional for MVP) --- ## Setup ### 1. Install dependencies ```bash npm install cd infra && npm install ``` ### 2. Configure environment ```bash cp .env.example .env # Edit .env with your values ``` Key variables: | Variable | Where to get it | |---|---| | `GOOGLE_CLIENT_ID` | [Google Cloud Console](https://console.cloud.google.com/apis/credentials) → OAuth 2.0 Client ID | | `ALLOWED_DOMAINS` | Your company's Google Workspace domain, e.g. `acme.com` | | `ALLOWED_ORIGIN` | Your frontend URL, e.g. `https://app.acme.com` | | `EDI_PARTNERSHIP_ID` | AWS B2B Data Interchange → Partnerships | | `EDI_TRANSFORMER_ID` | AWS B2B Data Interchange → Transformers (X12 810) | | `EDI_SENDER_ID` | Your ISA Sender ID (padded to 15 chars) | | `EDI_RECEIVER_ID` | Your trading partner's ISA ID | ### 3. Set up Google OAuth 1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials) 2. Create a project (or use existing) 3. Enable the "Google Identity" API 4. Create an **OAuth 2.0 Client ID** → Web Application 5. Add your frontend domain to **Authorized JavaScript origins** - `http://localhost:3000` (dev) - `https://your-production-domain.com` (prod) 6. No redirect URIs needed — using [Google Identity Services](https://developers.google.com/identity/gsi/web) (new flow) 7. Copy the Client ID to `GOOGLE_CLIENT_ID` ### 4. Set up AWS B2B EDI (optional — app works without it) 1. Go to [AWS B2B Data Interchange](https://console.aws.amazon.com/b2bi/home) 2. Create a **Profile** (your company details) 3. Create a **Partnership** with your trading partner 4. Create a **Capability** → choose X12 / 810 Invoice 5. Create a **Transformer** for inbound/outbound mapping 6. Copy the Partnership ID and Transformer ID to your `.env` ### 5. Deploy ```bash # Load env vars and deploy export $(cat .env | xargs) npm run deploy ``` CDK will output your **API Gateway URL**: ``` Outputs: LedgerFlow.ApiUrl = https://abc123.execute-api.us-east-1.amazonaws.com ``` ### 6. Connect the frontend In your `accounting-app.html`, add before ``: ```html ``` Or set these via your hosting environment (Cloudflare Pages, S3+CloudFront, etc.). Replace `localStorage` calls in `accounting-app.html` with `window.LedgerFlowAPI.*` calls (the API client is defined in `frontend-updates/login.html`). --- ## Local Development For local testing, use AWS SAM or run Lambdas directly: ```bash # Test a Lambda locally node -e " const fn = require('./lambdas/pos'); fn.handler({ requestContext: { http: { method: 'GET' } }, rawPath: '/pos', queryStringParameters: {} }, {}, { email: 'dev@test.com' }).then(console.log); " ``` Or use [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/): ```bash sam local start-api --template infra/template.yaml ``` --- ## Security Notes - **Google tokens** are verified on every request by the Lambda Authorizer — they cannot be forged - **AWS credentials** never leave Lambda environment variables — the frontend only holds a Google ID token - **DynamoDB access** is scoped per Lambda via least-privilege IAM roles (CDK handles this) - **ALLOWED_DOMAINS** restricts login to your company's Google Workspace — set this in production - **ALLOWED_ORIGIN** restricts CORS to your frontend domain — set this before going to production - **Session TTL**: DynamoDB sessions auto-expire after 7 days of inactivity --- ## Extending **Add a new Lambda route:** 1. Create `lambdas/myroute/index.js` exporting `handler` 2. Add to `infra/lib/ledgerflow-stack.js`: ```js const myFn = new lambda.Function(this, "MyFn", { ...lambdaDefaults, code: lambda.Code.fromAsset("../lambdas/myroute"), ... }); addRoutes("/myroute", myFn); ``` 3. Re-deploy: `npm run deploy` **Add a new DynamoDB table:** ```js const myTable = new dynamo.Table(this, "MyTable", { tableName: "ledgerflow-mytable", partitionKey: { name: "id", type: dynamo.AttributeType.STRING }, billingMode: dynamo.BillingMode.PAY_PER_REQUEST, }); myTable.grantReadWriteData(myFn); ``` --- ## File Structure ``` ledgerflow-backend/ ├── lambdas/ │ ├── authorizer/ Google JWT Lambda Authorizer │ │ └── index.js │ ├── auth/ Auth endpoints (/auth/me, /auth/config) │ │ └── index.js │ ├── pos/ Purchase Orders CRUD + DynamoDB import │ │ └── index.js │ ├── invoices/ Invoices CRUD + PO balance tracking │ │ └── index.js │ └── edi/ AWS B2B EDI submission + transaction log │ └── index.js ├── shared/ │ ├── index.js Shared utilities (responses, DynamoDB client, helpers) │ └── package.json ├── infra/ │ ├── bin/app.js CDK entry point │ ├── lib/ CDK stack (all AWS resources) │ ├── cdk.json │ └── package.json ├── .env.example ├── .gitignore └── package.json ``` --- ## EDI (X12 810) Details The EDI lambda generates X12 004010 810 Invoice documents for Amazon Payee Central. **Address handling:** - **Bill-To (N1\*BT)**: Hardcoded to Amazon.com Services LLC, 410 Terry Ave N, Seattle WA 98109-5210 - **Ship-To (N1\*ST)**: Pulled from the external `purchase-orders` DynamoDB table via `ship_to` field (Coupa PO data) - **Remit-To / Payee (N1\*RI, N1\*PE)**: From company settings **Submit payload:** ```json { "invoiceId": "INV-0001-id", "txType": "810", "billTo": { "name": "Override Name", "address": "123 Main St, City, ST 12345, US" } } ``` `billTo` is optional — defaults to Amazon HQ if omitted.