All checks were successful
Build and Push Docker Images / docker (push) Successful in 2m28s
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>
213 lines
8.3 KiB
Markdown
213 lines
8.3 KiB
Markdown
# 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`
|