Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions nodes/Crossmint/Crossmint.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import {
transferToken,
signTransaction
} from './actions/wallet';
import {
checkoutFields,
findProduct,
purchaseProduct
import {
checkoutFields,
findProduct,
purchaseProduct,
bookFlight
} from './actions/checkout';
import {
nftFields,
Expand Down Expand Up @@ -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,
Expand Down
135 changes: 135 additions & 0 deletions nodes/Crossmint/actions/checkout/bookFlight.operation.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
// 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);
Comment on lines +37 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing validation for passport-related fields (passportCountry, passportExpiry) which are required for flight bookings

Suggested change
// 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);
// 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);
validateRequiredField(passportCountry, 'Passport Country', context, itemIndex);
validateRequiredField(passportExpiry, 'Passport Expiry', 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);
Comment on lines +132 to +133
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Missing error logging before re-throwing the error for debugging purposes

Suggested change
} catch (error: any) {
throw new NodeApiError(context.getNode(), error);
} catch (error: any) {
console.error('[bookFlight] Flight booking failed with error:', error);
throw new NodeApiError(context.getNode(), error);

Context Used: Rule from dashboard - Include console.error logging statements when providers fail, using the format `console.error('[Clas... (source)

}
}
Comment on lines +46 to +135
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Function is 89 lines long and should be broken down into smaller helper functions for better maintainability

Context Used: Rule from dashboard - When a function grows to 80+ lines, extract helper functions to improve readability and maintainabil... (source)

7 changes: 7 additions & 0 deletions nodes/Crossmint/actions/checkout/findProduct.operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ 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,
api: CrossmintApi,
itemIndex: number,
): Promise<any> {
const platform = context.getNodeParameter('platform', itemIndex) as string;

// If platform is flight, delegate to bookFlight operation
if (platform === 'flight') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: String literal comparison instead of using enum constant. Consider defining platform constants for consistency and type safety.

Context Used: Rule from dashboard - Use enum constants (e.g., Chain.SOLANA, Chain.BASE) instead of string literals when comparing chain ... (source)

return await bookFlight(context, api, itemIndex);
}
Comment on lines +16 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The findProduct function now handles both product finding and flight booking, violating single responsibility principle. Consider renaming this function to something more generic like 'processOrder' or creating separate operations entirely.


const productIdentifier = context.getNodeParameter('productIdentifier', itemIndex) as string;
const recipientEmail = context.getNodeParameter('recipientEmail', itemIndex) as string;

Expand Down
Loading
Loading