2026-04-02 18:19:38 -04:00
|
|
|
# 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
|
2026-04-03 14:29:32 -04:00
|
|
|
│ ├── GET /pos list (paginated, auto-syncs from external table)
|
2026-04-02 18:19:38 -04:00
|
|
|
│ ├── POST /pos create
|
|
|
|
|
│ ├── GET /pos/:id get one
|
|
|
|
|
│ ├── PUT /pos/:id update
|
|
|
|
|
│ ├── DELETE /pos/:id delete
|
2026-04-03 14:29:32 -04:00
|
|
|
│ └── POST /pos/import bulk import
|
|
|
|
|
│
|
|
|
|
|
├── DynamoDB Streams ← PO Sync Lambda (ledgerflow-po-sync)
|
|
|
|
|
│ purchase-orders → ledgerflow-pos (near-real-time upsert)
|
2026-04-02 18:19:38 -04:00
|
|
|
│
|
|
|
|
|
├── /invoices/** ← Invoices Lambda
|
|
|
|
|
│ ├── GET /invoices list
|
|
|
|
|
│ ├── POST /invoices create (bills against PO)
|
|
|
|
|
│ ├── GET /invoices/:id get one
|
2026-04-03 11:40:37 -04:00
|
|
|
│ ├── PUT /invoices/:id update (full edit with line items; marks modified_after_send if already EDI-submitted)
|
2026-04-02 18:19:38 -04:00
|
|
|
│ ├── PATCH /invoices/:id/status status transition
|
2026-04-03 11:40:37 -04:00
|
|
|
│ └── DELETE /invoices/:id delete (any non-paid invoice; reverses PO billed amount)
|
2026-04-02 18:19:38 -04:00
|
|
|
│
|
|
|
|
|
├── /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:
|
2026-04-03 14:29:32 -04:00
|
|
|
purchase-orders Coupa POs (DynamoDB Streams → auto-sync to ledgerflow-pos)
|
2026-04-02 18:19:38 -04:00
|
|
|
|
|
|
|
|
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`
|
|
|
|
|
|
2026-04-03 14:29:32 -04:00
|
|
|
### 5. Enable DynamoDB Streams (for real-time PO sync)
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# Enable streams on the external purchase-orders table
|
|
|
|
|
aws dynamodb update-table --table-name purchase-orders \
|
|
|
|
|
--stream-specification StreamEnabled=true,StreamViewType=NEW_IMAGE
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Copy the stream ARN from the output — you'll pass it during deploy.
|
|
|
|
|
|
|
|
|
|
### 6. Deploy
|
2026-04-02 18:19:38 -04:00
|
|
|
|
|
|
|
|
```bash
|
2026-04-03 14:29:32 -04:00
|
|
|
# Load env vars and deploy (include stream ARN for real-time sync)
|
2026-04-02 18:19:38 -04:00
|
|
|
export $(cat .env | xargs)
|
2026-04-03 14:29:32 -04:00
|
|
|
npx cdk deploy -c purchaseOrdersStreamArn="arn:aws:dynamodb:us-east-1:ACCOUNT:table/purchase-orders/stream/..."
|
2026-04-02 18:19:38 -04:00
|
|
|
```
|
|
|
|
|
|
2026-04-03 14:29:32 -04:00
|
|
|
If you omit the stream ARN, the PO Lambda falls back to on-demand sync (scans the external table on every GET /pos request).
|
|
|
|
|
|
2026-04-02 18:19:38 -04:00
|
|
|
CDK will output your **API Gateway URL**:
|
|
|
|
|
```
|
|
|
|
|
Outputs:
|
|
|
|
|
LedgerFlow.ApiUrl = https://abc123.execute-api.us-east-1.amazonaws.com
|
|
|
|
|
```
|
|
|
|
|
|
2026-04-03 14:29:32 -04:00
|
|
|
### 7. Connect the frontend
|
2026-04-02 18:19:38 -04:00
|
|
|
|
|
|
|
|
In your `accounting-app.html`, add before `</body>`:
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
<script>
|
|
|
|
|
window.LEDGERFLOW_API_URL = "https://abc123.execute-api.us-east-1.amazonaws.com";
|
|
|
|
|
window.GOOGLE_CLIENT_ID = "your-client-id.apps.googleusercontent.com";
|
|
|
|
|
</script>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-03 14:29:32 -04:00
|
|
|
│ ├── pos/ Purchase Orders CRUD + on-demand sync fallback
|
|
|
|
|
│ │ └── index.js
|
|
|
|
|
│ ├── po-sync/ DynamoDB Streams handler (purchase-orders → ledgerflow-pos)
|
2026-04-02 18:19:38 -04:00
|
|
|
│ │ └── 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.
|