puffin-app/docs/NOCODB_SCHEMA.md

213 lines
8.3 KiB
Markdown
Raw Normal View History

Add NocoDB integration for order management with comprehensive Stripe webhook logging Features: - Complete NocoDB schema with 42 fields supporting B2B and B2C customers - Server-side NocoDB client (REST API integration) - Stripe session data mapper with automatic field mapping - Enhanced webhook handler with comprehensive logging - Automatic order creation in NocoDB after payment - Fulfillment data updates with Wren order IDs - Support for business customers (VAT/EIN, business names) - Complete billing address capture - Non-blocking error handling (webhook succeeds even if NocoDB fails) Files Added: - server/utils/nocodbClient.js - NocoDB REST API client - server/utils/nocodbMapper.js - Stripe to NocoDB data mapper - docs/NOCODB_SCHEMA.md - Complete field reference (42 columns) - docs/NOCODB_INTEGRATION_GUIDE.md - Testing and deployment guide - docs/TESTING_STRIPE_WEBHOOK.md - Webhook testing instructions - docs/STRIPE_INTEGRATION_SUMMARY.md - Project overview Files Modified: - server/routes/webhooks.js - Added NocoDB integration and enhanced logging - src/types.ts - Updated OrderRecord interface with new fields - src/api/nocodbClient.ts - Added createOrder() method - .env.example - Added NocoDB configuration template Schema includes: - Payment tracking (Stripe session/intent/customer IDs, amounts, fees) - Carbon offset details (tons, portfolio, Wren order ID) - Customer information (name, email, phone, business name) - Tax ID collection (VAT, EIN, etc.) - Complete billing address - Optional vessel/trip details for yacht calculations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 16:35:15 +01:00
# NocoDB Orders Table Schema
## Proposed Schema (Updated 2025-11-03)
### Core Identification
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `Id` | Number | Yes (auto) | NocoDB record ID |
| `CreatedAt` | DateTime | Yes (auto) | Record creation timestamp |
| `UpdatedAt` | DateTime | Yes (auto) | Record last update timestamp |
| `orderId` | String | Yes | Unique order identifier (UUID) |
| `status` | Enum | Yes | Order status: `pending`, `paid`, `fulfilled`, `cancelled` |
| `source` | String | No | Order source: `web`, `mobile-app`, `manual`, `api` |
### Payment Information
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `stripeSessionId` | String | No | Stripe Checkout Session ID |
| `stripePaymentIntent` | String | No | Stripe Payment Intent ID (for refunds) |
| `baseAmount` | String | Yes | Pre-fee amount in cents (e.g., "160174") |
| `processingFee` | String | Yes | Stripe processing fee in cents (e.g., "4805") |
| `totalAmount` | String | Yes | Total charged amount in cents (baseAmount + processingFee) |
| `currency` | String | Yes | Currency code: `USD`, `EUR`, `GBP`, `CHF` |
| `amountUSD` | String | No | Amount converted to USD for reporting |
| `paymentMethod` | String | No | Payment method type (e.g., "card", "bank_transfer") |
### Carbon Offset Details
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `co2Tons` | String | Yes | Tons of CO2 offset (e.g., "6.73") |
| `portfolioId` | String | Yes | Wren portfolio ID (e.g., "37") |
| `portfolioName` | String | No | Human-readable portfolio name |
| `wrenOrderId` | String | No | Wren API order ID (populated after fulfillment) |
| `certificateUrl` | String | No | URL to offset certificate |
| `fulfilledAt` | DateTime | No | Timestamp when order was fulfilled with Wren |
### Customer Information
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `customerName` | String | Yes | Customer display name (business or individual) |
| `customerEmail` | String | Yes | Customer email address |
| `customerPhone` | String | No | Customer phone (if phone collection enabled in Stripe) |
| `businessName` | String | No | Business name for B2B purchases |
| `stripeCustomerId` | String | No | Stripe Customer ID (for recurring customers) |
| `taxIdType` | String | No | Tax ID type (e.g., "eu_vat", "us_ein", "gb_vat") |
| `taxIdValue` | String | No | Tax identification number |
| `billingCity` | String | No | Billing address city |
| `billingCountry` | String | No | Billing address country code (e.g., "FR") |
| `billingLine1` | String | No | Billing address line 1 |
| `billingLine2` | String | No | Billing address line 2 |
| `billingPostalCode` | String | No | Billing address postal/zip code |
| `billingState` | String | No | Billing address state/region |
### Vessel Information (Optional - for yacht calculations)
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `vesselName` | String | No | Name of vessel |
| `imoNumber` | String | No | IMO vessel identification number |
| `vesselType` | String | No | Type of vessel (e.g., "Motor Yacht") |
| `vesselLength` | String | No | Vessel length in meters |
### Trip Details (Optional - for trip-based calculations)
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `departurePort` | String | No | Departure port name |
| `arrivalPort` | String | No | Arrival port name |
| `distance` | String | No | Distance in nautical miles |
| `avgSpeed` | String | No | Average speed in knots |
| `duration` | String | No | Trip duration in hours |
| `enginePower` | String | No | Engine power in horsepower |
### Administrative
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `notes` | LongText | No | Internal admin notes |
---
## Changes from Original Schema
### ✅ Added Fields
**Payment & Stripe Integration:**
- `stripeSessionId` - Link to Stripe Checkout Session
- `stripePaymentIntent` - Link to Stripe Payment Intent
- `stripeCustomerId` - Reusable Stripe Customer ID
- `baseAmount` - Pre-fee amount from Stripe metadata
- `processingFee` - Stripe fee from metadata
**Billing Address:**
- `billingCity` - From Stripe address
- `billingCountry` - From Stripe address
- `billingLine1` - From Stripe address
- `billingLine2` - From Stripe address
- `billingPostalCode` - From Stripe address
- `billingState` - From Stripe address
**B2B Customer Support:**
- `businessName` - Business name for B2B purchases
- `taxIdType` - Tax ID type (eu_vat, us_ein, gb_vat, etc.)
- `taxIdValue` - Tax identification number
### ⚠️ Modified Fields
- `customerPhone` - Now optional, populated from Stripe if phone collection is enabled
- `customerName` - Now serves as display name (either business_name or individual_name)
### ❌ Removed Fields
- `customerCompany` - Not provided by Stripe, can be added manually if needed
- `paymentReference` - Redundant with `stripePaymentIntent`
### ⚠️ Made Optional
All vessel and trip fields are now optional since they only apply to specific order types (not all orders are for vessel trips).
---
## Stripe Webhook Mapping
When receiving Stripe webhook `checkout.session.completed`:
```typescript
{
// Payment Information
stripeSessionId: session.id,
stripePaymentIntent: session.payment_intent,
baseAmount: session.metadata.baseAmount, // in cents
processingFee: session.metadata.processingFee, // in cents
totalAmount: session.amount_total.toString(), // in cents
currency: session.currency,
paymentMethod: session.payment_method_types[0], // 'card', 'us_bank_account', etc.
// Carbon Offset Details
co2Tons: session.metadata.tons,
portfolioId: session.metadata.portfolioId,
// Customer Information
customerName: session.customer_details.name, // Display name (business or individual)
customerEmail: session.customer_details.email,
customerPhone: session.customer_details.phone, // if phone collection enabled
// Business Customer Fields (B2B)
businessName: session.customer_details.business_name, // For B2B purchases
stripeCustomerId: session.customer, // Reusable customer ID
// Tax Collection (if enabled)
taxIdType: session.customer_details.tax_ids?.[0]?.type, // 'eu_vat', 'us_ein', etc.
taxIdValue: session.customer_details.tax_ids?.[0]?.value, // Tax number
// Billing Address
billingCity: session.customer_details.address?.city,
billingCountry: session.customer_details.address?.country,
billingLine1: session.customer_details.address?.line1,
billingLine2: session.customer_details.address?.line2,
billingPostalCode: session.customer_details.address?.postal_code,
billingState: session.customer_details.address?.state,
// Order Status
status: 'paid'
}
```
### Real-World Example (Business Purchase)
From actual Stripe payload `evt_1SPPa3Pdj1mnVT5kscrqB21t`:
```typescript
{
stripeSessionId: "cs_test_b1HSYDGs73Ail2Vumu0qC3yu96ce9X4qnozsDr5hDwRndpZOsq8H47flLc",
stripePaymentIntent: "pi_3SPPa2Pdj1mnVT5k2qsmDiV1",
stripeCustomerId: "cus_TM7pU6vRGh0N5N",
baseAmount: "16023588", // $160,235.88
processingFee: "480708", // $4,807.08
totalAmount: "16504296", // $165,042.96
currency: "usd",
co2Tons: "673.26",
portfolioId: "37",
customerName: "LetsBe Solutions LLC", // Business name used as display name
customerEmail: "matt@letsbe.solutions",
customerPhone: "+33633219796",
businessName: "LetsBe Solutions LLC",
taxIdType: "eu_vat",
taxIdValue: "FRAB123456789",
billingLine1: "108 Avenue du Trois Septembre",
billingLine2: null,
billingCity: "Cap-d'Ail",
billingState: null,
billingPostalCode: "06320",
billingCountry: "FR",
paymentMethod: "card",
status: "paid"
}
```
---
## Field Type Notes
**Why String for numeric fields?**
NocoDB stores all custom fields as strings by default. Numeric calculations should be done in application code by parsing these strings. This prevents precision issues with currency and decimal values.
**Date/DateTime fields:**
- `CreatedAt`, `UpdatedAt`, `fulfilledAt` use NocoDB's DateTime type
- ISO 8601 format: `2025-11-03T14:30:00.000Z`
**Enum constraints:**
- `status`: Must be one of `pending`, `paid`, `fulfilled`, `cancelled`
- `currency`: Must be one of `USD`, `EUR`, `GBP`, `CHF`
- `source`: Typically `web`, `mobile-app`, `manual`, or `api`