From 6c4889cc140cd26f0d0d4abea3575fda7e5bbc05 Mon Sep 17 00:00:00 2001 From: manu Date: Thu, 18 Sep 2025 13:01:17 +0200 Subject: [PATCH] New Flight Book in Create Order --- nodes/Crossmint/Crossmint.node.ts | 12 +- .../actions/checkout/bookFlight.operation.ts | 135 ++++++ .../actions/checkout/findProduct.operation.ts | 7 + nodes/Crossmint/actions/checkout/index.ts | 394 ++++++++++++++++++ 4 files changed, 544 insertions(+), 4 deletions(-) create mode 100644 nodes/Crossmint/actions/checkout/bookFlight.operation.ts diff --git a/nodes/Crossmint/Crossmint.node.ts b/nodes/Crossmint/Crossmint.node.ts index b7cfb1a..daf5ec4 100644 --- a/nodes/Crossmint/Crossmint.node.ts +++ b/nodes/Crossmint/Crossmint.node.ts @@ -16,10 +16,11 @@ import { transferToken, signTransaction } from './actions/wallet'; -import { - checkoutFields, - findProduct, - purchaseProduct +import { + checkoutFields, + findProduct, + purchaseProduct, + bookFlight } from './actions/checkout'; import { nftFields, @@ -133,6 +134,9 @@ export class Crossmint implements INodeType { case 'purchaseProduct': result = await purchaseProduct(this, api, itemIndex); break; + case 'bookFlight': + result = await bookFlight(this, api, itemIndex); + break; default: throw new NodeOperationError(this.getNode(), `Unknown checkout operation: ${operation}`, { itemIndex, diff --git a/nodes/Crossmint/actions/checkout/bookFlight.operation.ts b/nodes/Crossmint/actions/checkout/bookFlight.operation.ts new file mode 100644 index 0000000..8377470 --- /dev/null +++ b/nodes/Crossmint/actions/checkout/bookFlight.operation.ts @@ -0,0 +1,135 @@ +import { IExecuteFunctions, NodeApiError } from 'n8n-workflow'; +import { CrossmintApi } from '../../transport/CrossmintApi'; +import { validateEmail, validateRequiredField } from '../../utils/validation'; + +export async function bookFlight( + context: IExecuteFunctions, + api: CrossmintApi, + itemIndex: number, +): Promise { + // Flight search parameters + const originIATA = context.getNodeParameter('originIATA', itemIndex) as string; + const destinationIATA = context.getNodeParameter('destinationIATA', itemIndex) as string; + const departureDate = context.getNodeParameter('departureDate', itemIndex) as string; + const cabinClass = context.getNodeParameter('cabinClass', itemIndex) as string; + const passengerCount = context.getNodeParameter('passengerCount', itemIndex) as number; + const flightIds = context.getNodeParameter('flightIds', itemIndex) as string; + + // Passenger details + const passengerTitle = context.getNodeParameter('passengerTitle', itemIndex) as string; + const passengerFirstName = context.getNodeParameter('passengerFirstName', itemIndex) as string; + const passengerLastName = context.getNodeParameter('passengerLastName', itemIndex) as string; + const passengerBirthDate = context.getNodeParameter('passengerBirthDate', itemIndex) as string; + const passengerGender = context.getNodeParameter('passengerGender', itemIndex) as string; + const passengerEmail = context.getNodeParameter('recipientEmail', itemIndex) as string; + const passengerPhone = context.getNodeParameter('passengerPhone', itemIndex) as string; + + // Passport details + const passportNumber = context.getNodeParameter('passportNumber', itemIndex) as string; + const passportCountry = context.getNodeParameter('passportCountry', itemIndex) as string; + const passportExpiry = context.getNodeParameter('passportExpiry', itemIndex) as string; + + // Payment details + const paymentMethod = context.getNodeParameter('paymentMethod', itemIndex) as string; + const paymentCurrency = context.getNodeParameter('paymentCurrency', itemIndex) as string; + const payerAddress = context.getNodeParameter('payerAddress', itemIndex) as string; + + // Validation + validateRequiredField(originIATA, 'Origin IATA', context, itemIndex); + validateRequiredField(destinationIATA, 'Destination IATA', context, itemIndex); + validateRequiredField(departureDate, 'Departure Date', context, itemIndex); + validateRequiredField(passengerFirstName, 'Passenger First Name', context, itemIndex); + validateRequiredField(passengerLastName, 'Passenger Last Name', context, itemIndex); + validateEmail(passengerEmail, context, itemIndex); + validateRequiredField(passportNumber, 'Passport Number', context, itemIndex); + + try { + // Step 1: Search for flights using Worldstore Search API + const flightIdsArray = flightIds.split(',').map(id => id.trim()); + + const searchBody = { + uid: { + originIATA: originIATA, + destinationIATA: destinationIATA, + cabinClass: cabinClass, + passenger_number: passengerCount, + departureFlightDetails: { + departureDate, + flightIds: flightIdsArray, + }, + }, + }; + + const searchResponse = await api.post('ws/search', searchBody, 'unstable'); + const { listings } = searchResponse; + + if (!listings || listings.length === 0) { + throw new Error('No flights found for the specified criteria'); + } + + // Use the first available listing + const selectedListing = listings[0]; + + // Step 2: Create Worldstore order with passenger details + const passengers = [{ + title: passengerTitle, + given_name: passengerFirstName, + family_name: passengerLastName, + born_on: passengerBirthDate, + gender: passengerGender, + email: passengerEmail, + phone_number: passengerPhone, + identity_documents: [{ + type: 'passport', + unique_identifier: passportNumber, + issuing_country_code: passportCountry, + expires_on: passportExpiry, + }], + }]; + + const wsOrderBody = { + sellerId: '1', + items: [{ + listingId: selectedListing.id, + listingParameters: { + passengers, + }, + }], + orderParameters: {}, + }; + + const wsOrderResponse = await api.post('ws/orders', wsOrderBody, 'unstable'); + const { order } = wsOrderResponse; + + // Step 3: Create Crossmint payment order + const payment: any = { + receiptEmail: passengerEmail, + method: paymentMethod, + currency: paymentCurrency, + }; + + if (payerAddress) { + payment.payerAddress = payerAddress; + } + + const crossmintOrderBody = { + recipient: { + email: passengerEmail, + }, + locale: 'en-US', + payment, + externalOrder: order, + }; + + const crossmintOrderResponse = await api.post('orders', crossmintOrderBody, '2022-06-09'); + + return { + flightSearch: searchResponse, + worldstoreOrder: wsOrderResponse, + crossmintOrder: crossmintOrderResponse, + selectedFlight: selectedListing, + }; + } catch (error: any) { + throw new NodeApiError(context.getNode(), error); + } +} \ No newline at end of file diff --git a/nodes/Crossmint/actions/checkout/findProduct.operation.ts b/nodes/Crossmint/actions/checkout/findProduct.operation.ts index 58e8d0f..bb44bd1 100644 --- a/nodes/Crossmint/actions/checkout/findProduct.operation.ts +++ b/nodes/Crossmint/actions/checkout/findProduct.operation.ts @@ -4,6 +4,7 @@ import { API_VERSIONS } from '../../utils/constants'; import { validateEmail, validateRequiredField, validateAddressFields } from '../../utils/validation'; import { buildProductLocator } from '../../utils/locators'; import { OrderCreateRequest } from '../../transport/types'; +import { bookFlight } from './bookFlight.operation'; export async function findProduct( context: IExecuteFunctions, @@ -11,6 +12,12 @@ export async function findProduct( itemIndex: number, ): Promise { const platform = context.getNodeParameter('platform', itemIndex) as string; + + // If platform is flight, delegate to bookFlight operation + if (platform === 'flight') { + return await bookFlight(context, api, itemIndex); + } + const productIdentifier = context.getNodeParameter('productIdentifier', itemIndex) as string; const recipientEmail = context.getNodeParameter('recipientEmail', itemIndex) as string; diff --git a/nodes/Crossmint/actions/checkout/index.ts b/nodes/Crossmint/actions/checkout/index.ts index 294da25..1bf26ca 100644 --- a/nodes/Crossmint/actions/checkout/index.ts +++ b/nodes/Crossmint/actions/checkout/index.ts @@ -2,6 +2,7 @@ import { INodeProperties } from 'n8n-workflow'; export { findProduct } from './findProduct.operation'; export { purchaseProduct } from './purchaseProduct.operation'; +export { bookFlight } from './bookFlight.operation'; export const checkoutFields: INodeProperties[] = [ { @@ -23,6 +24,12 @@ export const checkoutFields: INodeProperties[] = [ description: 'Purchase a product with automated checkout', action: 'Pay order', }, + { + name: 'Book Flight', + value: 'bookFlight', + description: 'Search and book flights using Worldstore', + action: 'Book flight', + }, ], default: 'findProduct', }, @@ -34,6 +41,7 @@ export const checkoutFields: INodeProperties[] = [ options: [ { name: 'Amazon', value: 'amazon', description: 'Amazon marketplace' }, { name: 'Shopify', value: 'shopify', description: 'Shopify store' }, + { name: 'Flight', value: 'flight', description: 'Flight booking via Worldstore' }, ], default: 'amazon', description: 'E-commerce platform for the purchase', @@ -116,6 +124,172 @@ export const checkoutFields: INodeProperties[] = [ description: 'Full Shopify product URL (will use default variant)', required: true, }, + + // ---- Flight fields for Create Order (when platform is flight) + { + displayName: 'Origin Airport (IATA)', + name: 'originIATA', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: 'JFK', + description: '3-letter IATA airport code for departure', + required: true, + }, + { + displayName: 'Destination Airport (IATA)', + name: 'destinationIATA', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: 'ATH', + description: '3-letter IATA airport code for arrival', + required: true, + }, + { + displayName: 'Departure Date', + name: 'departureDate', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: '2025-07-19', + description: 'Departure date in YYYY-MM-DD format', + required: true, + }, + { + displayName: 'Flight IDs', + name: 'flightIds', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: 'AY4161,LH123', + description: 'Comma-separated list of flight IDs to search for', + required: true, + }, + { + displayName: 'Cabin Class', + name: 'cabinClass', + type: 'options', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + options: [ + { name: 'Economy', value: 'economy' }, + { name: 'Premium Economy', value: 'premium_economy' }, + { name: 'Business', value: 'business' }, + { name: 'First', value: 'first' }, + ], + default: 'economy', + description: 'Cabin class for the flight', + required: true, + }, + { + displayName: 'Number of Passengers', + name: 'passengerCount', + type: 'number', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: 1, + description: 'Number of passengers traveling', + required: true, + }, + + // Passenger details for flights in Create Order + { + displayName: 'Passenger Title', + name: 'passengerTitle', + type: 'options', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + options: [ + { name: 'Mr', value: 'mr' }, + { name: 'Mrs', value: 'mrs' }, + { name: 'Ms', value: 'ms' }, + { name: 'Dr', value: 'dr' }, + ], + default: 'mr', + required: true, + }, + { + displayName: 'Passenger First Name', + name: 'passengerFirstName', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: 'John', + description: 'Passenger first name as on passport', + required: true, + }, + { + displayName: 'Passenger Last Name', + name: 'passengerLastName', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: 'Doe', + description: 'Passenger last name as on passport', + required: true, + }, + { + displayName: 'Passenger Birth Date', + name: 'passengerBirthDate', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: '1980-01-01', + description: 'Passenger birth date in YYYY-MM-DD format', + required: true, + }, + { + displayName: 'Passenger Gender', + name: 'passengerGender', + type: 'options', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + options: [ + { name: 'Male', value: 'm' }, + { name: 'Female', value: 'f' }, + { name: 'Other', value: 'other' }, + ], + default: 'm', + required: true, + }, + { + displayName: 'Passenger Phone', + name: 'passengerPhone', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: '+14155552671', + description: 'Passenger phone number with country code', + required: true, + }, + + // Passport Information for flights in Create Order + { + displayName: 'Passport Number', + name: 'passportNumber', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: '123456789', + required: true, + }, + { + displayName: 'Passport Country', + name: 'passportCountry', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: 'US', + description: 'Two-letter country code of passport issuing country', + required: true, + }, + { + displayName: 'Passport Expiry Date', + name: 'passportExpiry', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['findProduct'], platform: ['flight'] } }, + default: '', + placeholder: '2030-04-24', + description: 'Passport expiry date in YYYY-MM-DD format', + required: true, + }, { displayName: 'Recipient Email', name: 'recipientEmail', @@ -277,4 +451,224 @@ export const checkoutFields: INodeProperties[] = [ description: 'Agent wallet address for crypto payments - must be a Crossmint managed wallet with USDC funds', required: true, }, + + // ---- Flight Booking fields + { + displayName: 'Origin Airport (IATA)', + name: 'originIATA', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: 'JFK', + description: '3-letter IATA airport code for departure', + required: true, + }, + { + displayName: 'Destination Airport (IATA)', + name: 'destinationIATA', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: 'ATH', + description: '3-letter IATA airport code for arrival', + required: true, + }, + { + displayName: 'Departure Date', + name: 'departureDate', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: '2025-07-19', + description: 'Departure date in YYYY-MM-DD format', + required: true, + }, + { + displayName: 'Cabin Class', + name: 'cabinClass', + type: 'options', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + options: [ + { name: 'Economy', value: 'economy' }, + { name: 'Premium Economy', value: 'premium_economy' }, + { name: 'Business', value: 'business' }, + { name: 'First', value: 'first' }, + ], + default: 'economy', + description: 'Cabin class for the flight', + required: true, + }, + { + displayName: 'Number of Passengers', + name: 'passengerCount', + type: 'number', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: 1, + description: 'Number of passengers traveling', + required: true, + }, + { + displayName: 'Flight IDs', + name: 'flightIds', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: 'AY4161,LH123', + description: 'Comma-separated list of flight IDs to search for', + required: true, + }, + + // Passenger Information + { + displayName: 'Passenger Title', + name: 'passengerTitle', + type: 'options', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + options: [ + { name: 'Mr', value: 'mr' }, + { name: 'Mrs', value: 'mrs' }, + { name: 'Ms', value: 'ms' }, + { name: 'Dr', value: 'dr' }, + ], + default: 'mr', + description: 'Passenger title', + required: true, + }, + { + displayName: 'Passenger First Name', + name: 'passengerFirstName', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: 'John', + description: 'Passenger first name as on passport', + required: true, + }, + { + displayName: 'Passenger Last Name', + name: 'passengerLastName', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: 'Doe', + description: 'Passenger last name as on passport', + required: true, + }, + { + displayName: 'Passenger Birth Date', + name: 'passengerBirthDate', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: '1980-01-01', + description: 'Passenger birth date in YYYY-MM-DD format', + required: true, + }, + { + displayName: 'Passenger Gender', + name: 'passengerGender', + type: 'options', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + options: [ + { name: 'Male', value: 'm' }, + { name: 'Female', value: 'f' }, + { name: 'Other', value: 'other' }, + ], + default: 'm', + description: 'Passenger gender', + required: true, + }, + { + displayName: 'Passenger Email', + name: 'passengerEmail', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: 'john@example.com', + description: 'Passenger email address', + required: true, + }, + { + displayName: 'Passenger Phone', + name: 'passengerPhone', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: '+14155552671', + description: 'Passenger phone number with country code', + required: true, + }, + + // Passport Information + { + displayName: 'Passport Number', + name: 'passportNumber', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: '123456789', + description: 'Passport number', + required: true, + }, + { + displayName: 'Passport Country', + name: 'passportCountry', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: 'US', + description: 'Two-letter country code of passport issuing country', + required: true, + }, + { + displayName: 'Passport Expiry Date', + name: 'passportExpiry', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: '2030-04-24', + description: 'Passport expiry date in YYYY-MM-DD format', + required: true, + }, + + // Payment Information for Flight Booking + { + displayName: 'Payment Chain', + name: 'paymentMethod', + type: 'options', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + options: [ + { name: 'Arbitrum Sepolia', value: 'arbitrum-sepolia', description: 'Arbitrum testnet' }, + { name: 'Base Sepolia', value: 'base-sepolia', description: 'Base testnet' }, + { name: 'Ethereum Sepolia', value: 'ethereum-sepolia', description: 'Ethereum testnet' }, + { name: 'Optimism Sepolia', value: 'optimism-sepolia', description: 'Optimism testnet' }, + { name: 'Polygon Amoy', value: 'polygon-amoy', description: 'Polygon testnet' }, + { name: 'World Chain Sepolia', value: 'world-chain-sepolia', description: 'World Chain testnet' }, + ], + default: 'base-sepolia', + description: 'Blockchain network for payment', + required: true, + }, + { + displayName: 'Payment Currency', + name: 'paymentCurrency', + type: 'options', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + options: [ + { name: 'USDC', value: 'usdc', description: 'USD Coin' }, + ], + default: 'usdc', + description: 'Cryptocurrency for payment', + required: true, + }, + { + displayName: 'Payer Wallet Address', + name: 'payerAddress', + type: 'string', + displayOptions: { show: { resource: ['checkout'], operation: ['bookFlight'] } }, + default: '', + placeholder: '0x1234567890123456789012345678901234567890', + description: 'Wallet address that will pay for the flight', + required: true, + }, ];