322 lines
7.6 KiB
TypeScript
322 lines
7.6 KiB
TypeScript
|
|
import { PricingCalculation } from './PricingService';
|
||
|
|
|
||
|
|
export interface PurchaseStep {
|
||
|
|
id: string;
|
||
|
|
title: string;
|
||
|
|
description: string;
|
||
|
|
completed: boolean;
|
||
|
|
current: boolean;
|
||
|
|
error?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface PurchaseFlowState {
|
||
|
|
currentStep: number;
|
||
|
|
steps: PurchaseStep[];
|
||
|
|
calculation: PricingCalculation | null;
|
||
|
|
paymentIntentId: string | null;
|
||
|
|
clientSecret: string | null;
|
||
|
|
error: any;
|
||
|
|
isProcessing: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class PurchaseFlowService {
|
||
|
|
private static readonly STEPS: Omit<PurchaseStep, 'completed' | 'current' | 'error'>[] = [
|
||
|
|
{
|
||
|
|
id: 'quantity-selection',
|
||
|
|
title: 'Select Tokens',
|
||
|
|
description: 'Choose the number of tokens you want to purchase'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'pricing-calculation',
|
||
|
|
title: 'Calculate Price',
|
||
|
|
description: 'Review pricing and available discounts'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'payment-method',
|
||
|
|
title: 'Payment Method',
|
||
|
|
description: 'Select your preferred payment method'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'payment-processing',
|
||
|
|
title: 'Process Payment',
|
||
|
|
description: 'Complete your payment securely'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'confirmation',
|
||
|
|
title: 'Confirmation',
|
||
|
|
description: 'Payment successful and tokens allocated'
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize the purchase flow
|
||
|
|
*/
|
||
|
|
static initializeFlow(): PurchaseFlowState {
|
||
|
|
const steps = this.STEPS.map((step, index) => ({
|
||
|
|
...step,
|
||
|
|
completed: false,
|
||
|
|
current: index === 0,
|
||
|
|
error: undefined
|
||
|
|
}));
|
||
|
|
|
||
|
|
return {
|
||
|
|
currentStep: 0,
|
||
|
|
steps,
|
||
|
|
calculation: null,
|
||
|
|
paymentIntentId: null,
|
||
|
|
clientSecret: null,
|
||
|
|
error: null,
|
||
|
|
isProcessing: false
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Move to the next step
|
||
|
|
*/
|
||
|
|
static nextStep(state: PurchaseFlowState): PurchaseFlowState {
|
||
|
|
const newState = { ...state };
|
||
|
|
|
||
|
|
if (newState.currentStep < newState.steps.length - 1) {
|
||
|
|
// Mark current step as completed
|
||
|
|
newState.steps[newState.currentStep] = {
|
||
|
|
...newState.steps[newState.currentStep],
|
||
|
|
completed: true,
|
||
|
|
current: false
|
||
|
|
};
|
||
|
|
|
||
|
|
// Move to next step
|
||
|
|
newState.currentStep += 1;
|
||
|
|
newState.steps[newState.currentStep] = {
|
||
|
|
...newState.steps[newState.currentStep],
|
||
|
|
current: true
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return newState;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Move to the previous step
|
||
|
|
*/
|
||
|
|
static previousStep(state: PurchaseFlowState): PurchaseFlowState {
|
||
|
|
const newState = { ...state };
|
||
|
|
|
||
|
|
if (newState.currentStep > 0) {
|
||
|
|
// Mark current step as not current
|
||
|
|
newState.steps[newState.currentStep] = {
|
||
|
|
...newState.steps[newState.currentStep],
|
||
|
|
current: false
|
||
|
|
};
|
||
|
|
|
||
|
|
// Move to previous step
|
||
|
|
newState.currentStep -= 1;
|
||
|
|
newState.steps[newState.currentStep] = {
|
||
|
|
...newState.steps[newState.currentStep],
|
||
|
|
current: true,
|
||
|
|
completed: false // Allow editing previous step
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return newState;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Jump to a specific step
|
||
|
|
*/
|
||
|
|
static goToStep(state: PurchaseFlowState, stepIndex: number): PurchaseFlowState {
|
||
|
|
const newState = { ...state };
|
||
|
|
|
||
|
|
if (stepIndex >= 0 && stepIndex < newState.steps.length) {
|
||
|
|
// Reset all steps
|
||
|
|
newState.steps = newState.steps.map((step, index) => ({
|
||
|
|
...step,
|
||
|
|
completed: index < stepIndex,
|
||
|
|
current: index === stepIndex,
|
||
|
|
error: undefined
|
||
|
|
}));
|
||
|
|
|
||
|
|
newState.currentStep = stepIndex;
|
||
|
|
}
|
||
|
|
|
||
|
|
return newState;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set error for current step
|
||
|
|
*/
|
||
|
|
static setStepError(state: PurchaseFlowState, error: any): PurchaseFlowState {
|
||
|
|
const newState = { ...state };
|
||
|
|
|
||
|
|
newState.steps[newState.currentStep] = {
|
||
|
|
...newState.steps[newState.currentStep],
|
||
|
|
error: error?.message || 'An error occurred'
|
||
|
|
};
|
||
|
|
|
||
|
|
newState.error = error;
|
||
|
|
return newState;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clear error for current step
|
||
|
|
*/
|
||
|
|
static clearStepError(state: PurchaseFlowState): PurchaseFlowState {
|
||
|
|
const newState = { ...state };
|
||
|
|
|
||
|
|
newState.steps[newState.currentStep] = {
|
||
|
|
...newState.steps[newState.currentStep],
|
||
|
|
error: undefined
|
||
|
|
};
|
||
|
|
|
||
|
|
newState.error = null;
|
||
|
|
return newState;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set processing state
|
||
|
|
*/
|
||
|
|
static setProcessing(state: PurchaseFlowState, isProcessing: boolean): PurchaseFlowState {
|
||
|
|
return {
|
||
|
|
...state,
|
||
|
|
isProcessing
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set calculation data
|
||
|
|
*/
|
||
|
|
static setCalculation(state: PurchaseFlowState, calculation: PricingCalculation): PurchaseFlowState {
|
||
|
|
return {
|
||
|
|
...state,
|
||
|
|
calculation
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set payment intent data
|
||
|
|
*/
|
||
|
|
static setPaymentIntent(state: PurchaseFlowState, paymentIntentId: string, clientSecret: string): PurchaseFlowState {
|
||
|
|
return {
|
||
|
|
...state,
|
||
|
|
paymentIntentId,
|
||
|
|
clientSecret
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Complete the purchase flow
|
||
|
|
*/
|
||
|
|
static completeFlow(state: PurchaseFlowState): PurchaseFlowState {
|
||
|
|
const newState = { ...state };
|
||
|
|
|
||
|
|
// Mark all steps as completed
|
||
|
|
newState.steps = newState.steps.map(step => ({
|
||
|
|
...step,
|
||
|
|
completed: true,
|
||
|
|
current: false
|
||
|
|
}));
|
||
|
|
|
||
|
|
newState.isProcessing = false;
|
||
|
|
newState.error = null;
|
||
|
|
|
||
|
|
return newState;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reset the flow to initial state
|
||
|
|
*/
|
||
|
|
static resetFlow(): PurchaseFlowState {
|
||
|
|
return this.initializeFlow();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get current step info
|
||
|
|
*/
|
||
|
|
static getCurrentStep(state: PurchaseFlowState): PurchaseStep | null {
|
||
|
|
return state.steps[state.currentStep] || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if flow can proceed to next step
|
||
|
|
*/
|
||
|
|
static canProceed(state: PurchaseFlowState): boolean {
|
||
|
|
const currentStep = this.getCurrentStep(state);
|
||
|
|
if (!currentStep) return false;
|
||
|
|
|
||
|
|
switch (currentStep.id) {
|
||
|
|
case 'quantity-selection':
|
||
|
|
return state.calculation !== null;
|
||
|
|
case 'pricing-calculation':
|
||
|
|
return state.calculation !== null;
|
||
|
|
case 'payment-method':
|
||
|
|
return true; // Payment method selection is always valid
|
||
|
|
case 'payment-processing':
|
||
|
|
return state.clientSecret !== null;
|
||
|
|
case 'confirmation':
|
||
|
|
return true; // Confirmation step is always valid
|
||
|
|
default:
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get progress percentage
|
||
|
|
*/
|
||
|
|
static getProgress(state: PurchaseFlowState): number {
|
||
|
|
const completedSteps = state.steps.filter(step => step.completed).length;
|
||
|
|
return (completedSteps / state.steps.length) * 100;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if flow is completed
|
||
|
|
*/
|
||
|
|
static isCompleted(state: PurchaseFlowState): boolean {
|
||
|
|
return state.steps.every(step => step.completed);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get step by ID
|
||
|
|
*/
|
||
|
|
static getStepById(state: PurchaseFlowState, stepId: string): PurchaseStep | null {
|
||
|
|
return state.steps.find(step => step.id === stepId) || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate current step
|
||
|
|
*/
|
||
|
|
static validateCurrentStep(state: PurchaseFlowState): {
|
||
|
|
isValid: boolean;
|
||
|
|
error?: string;
|
||
|
|
} {
|
||
|
|
const currentStep = this.getCurrentStep(state);
|
||
|
|
if (!currentStep) {
|
||
|
|
return { isValid: false, error: 'Invalid step' };
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (currentStep.id) {
|
||
|
|
case 'quantity-selection':
|
||
|
|
if (!state.calculation) {
|
||
|
|
return { isValid: false, error: 'Please select a quantity' };
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'pricing-calculation':
|
||
|
|
if (!state.calculation) {
|
||
|
|
return { isValid: false, error: 'Please calculate pricing first' };
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'payment-method':
|
||
|
|
// Payment method selection is always valid
|
||
|
|
break;
|
||
|
|
case 'payment-processing':
|
||
|
|
if (!state.clientSecret) {
|
||
|
|
return { isValid: false, error: 'Payment intent not created' };
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'confirmation':
|
||
|
|
// Confirmation step is always valid
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return { isValid: true };
|
||
|
|
}
|
||
|
|
}
|