ledgerflow-backend/README.md

282 lines
9.5 KiB
Markdown
Raw Permalink Normal View History

# 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, auto-syncs from external table)
│ ├── POST /pos create
│ ├── GET /pos/:id get one
│ ├── PUT /pos/:id update
│ ├── DELETE /pos/:id delete
│ └── POST /pos/import bulk import
│
├── DynamoDB Streams ← PO Sync Lambda (ledgerflow-po-sync)
│ purchase-orders → ledgerflow-pos (near-real-time upsert)
│
├── /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 (DynamoDB Streams → auto-sync to ledgerflow-pos)
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. 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
```bash
# Load env vars and deploy (include stream ARN for real-time sync)
export $(cat .env | xargs)
npx cdk deploy -c purchaseOrdersStreamArn="arn:aws:dynamodb:us-east-1:ACCOUNT:table/purchase-orders/stream/..."
```
If you omit the stream ARN, the PO Lambda falls back to on-demand sync (scans the external table on every GET /pos request).
CDK will output your **API Gateway URL**:
```
Outputs:
LedgerFlow.ApiUrl = https://abc123.execute-api.us-east-1.amazonaws.com
```
### 7. Connect the frontend
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
│ ├── pos/ Purchase Orders CRUD + on-demand sync fallback
│ │ └── index.js
│ ├── po-sync/ DynamoDB Streams handler (purchase-orders → ledgerflow-pos)
│ │ └── 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.