This commit is contained in:
2025-11-26 17:03:29 +01:00
parent ff3528e5ef
commit fef75008d8
147 changed files with 112370 additions and 5162 deletions

View File

@@ -8,7 +8,8 @@ import { StartAutomationSessionUseCase } from '@/packages/application/use-cases/
import { CheckAuthenticationUseCase } from '@/packages/application/use-cases/CheckAuthenticationUseCase';
import { InitiateLoginUseCase } from '@/packages/application/use-cases/InitiateLoginUseCase';
import { ClearSessionUseCase } from '@/packages/application/use-cases/ClearSessionUseCase';
import { loadAutomationConfig, getAutomationMode, AutomationMode } from '@/packages/infrastructure/config';
import { ConfirmCheckoutUseCase } from '@/packages/application/use-cases/ConfirmCheckoutUseCase';
import { loadAutomationConfig, getAutomationMode, AutomationMode, BrowserModeConfigLoader } from '@/packages/infrastructure/config';
import { PinoLogAdapter } from '@/packages/infrastructure/adapters/logging/PinoLogAdapter';
import { NoOpLogAdapter } from '@/packages/infrastructure/adapters/logging/NoOpLogAdapter';
import { loadLoggingConfig } from '@/packages/infrastructure/config/LoggingConfig';
@@ -16,6 +17,7 @@ import type { ISessionRepository } from '@/packages/application/ports/ISessionRe
import type { IScreenAutomation } from '@/packages/application/ports/IScreenAutomation';
import type { IAutomationEngine } from '@/packages/application/ports/IAutomationEngine';
import type { IAuthenticationService } from '@/packages/application/ports/IAuthenticationService';
import type { ICheckoutConfirmationPort } from '@/packages/application/ports/ICheckoutConfirmationPort';
import type { ILogger } from '@/packages/application/ports/ILogger';
export interface BrowserConnectionResult {
@@ -92,7 +94,11 @@ function getAdapterMode(envMode: AutomationMode): AutomationAdapterMode {
* @param logger - Logger instance for the adapter
* @returns PlaywrightAutomationAdapter instance (implements both IScreenAutomation and IAuthenticationService)
*/
function createBrowserAutomationAdapter(mode: AutomationMode, logger: ILogger): PlaywrightAutomationAdapter | MockBrowserAutomationAdapter {
function createBrowserAutomationAdapter(
mode: AutomationMode,
logger: ILogger,
browserModeConfigLoader: BrowserModeConfigLoader
): PlaywrightAutomationAdapter | MockBrowserAutomationAdapter {
const config = loadAutomationConfig();
// Resolve absolute template path for Electron environment
@@ -108,18 +114,28 @@ function createBrowserAutomationAdapter(mode: AutomationMode, logger: ILogger):
});
const adapterMode = getAdapterMode(mode);
logger.info('Creating browser automation adapter', { envMode: mode, adapterMode });
// Get browser mode configuration from provided loader
const browserModeConfig = browserModeConfigLoader.load();
logger.info('Creating browser automation adapter', {
envMode: mode,
adapterMode,
browserMode: browserModeConfig.mode,
browserModeSource: browserModeConfig.source,
});
switch (mode) {
case 'production':
case 'development':
return new PlaywrightAutomationAdapter(
{
headless: mode === 'production',
headless: browserModeConfig.mode === 'headless',
mode: adapterMode,
userDataDir: sessionDataPath,
},
logger.child({ adapter: 'Playwright', mode: adapterMode })
logger.child({ adapter: 'Playwright', mode: adapterMode }),
browserModeConfigLoader
);
case 'test':
@@ -139,7 +155,9 @@ export class DIContainer {
private checkAuthenticationUseCase: CheckAuthenticationUseCase | null = null;
private initiateLoginUseCase: InitiateLoginUseCase | null = null;
private clearSessionUseCase: ClearSessionUseCase | null = null;
private confirmCheckoutUseCase: ConfirmCheckoutUseCase | null = null;
private automationMode: AutomationMode;
private browserModeConfigLoader: BrowserModeConfigLoader;
private constructor() {
// Initialize logger first - it's needed by other components
@@ -153,8 +171,15 @@ export class DIContainer {
const config = loadAutomationConfig();
// Initialize browser mode config loader as singleton
this.browserModeConfigLoader = new BrowserModeConfigLoader();
this.sessionRepository = new InMemorySessionRepository();
this.browserAutomation = createBrowserAutomationAdapter(config.mode, this.logger);
this.browserAutomation = createBrowserAutomationAdapter(
config.mode,
this.logger,
this.browserModeConfigLoader
);
this.automationEngine = new MockAutomationEngineAdapter(
this.browserAutomation,
this.sessionRepository
@@ -241,6 +266,21 @@ export class DIContainer {
return null;
}
public setConfirmCheckoutUseCase(
checkoutConfirmationPort: ICheckoutConfirmationPort
): void {
// Create ConfirmCheckoutUseCase with checkout service from browser automation
// and the provided confirmation port
this.confirmCheckoutUseCase = new ConfirmCheckoutUseCase(
this.browserAutomation as any, // implements ICheckoutService
checkoutConfirmationPort
);
}
public getConfirmCheckoutUseCase(): ConfirmCheckoutUseCase | null {
return this.confirmCheckoutUseCase;
}
/**
* Initialize automation connection based on mode.
* In production/development mode, connects via Playwright browser automation.
@@ -292,6 +332,14 @@ export class DIContainer {
this.logger.info('DIContainer shutdown complete');
}
/**
* Get the browser mode configuration loader.
* Provides access to runtime browser mode control (headed/headless).
*/
public getBrowserModeConfigLoader(): BrowserModeConfigLoader {
return this.browserModeConfigLoader;
}
/**
* Reset the singleton instance (useful for testing with different configurations).
*/

View File

@@ -4,6 +4,7 @@ import { DIContainer } from './di-container';
import type { HostedSessionConfig } from '@/packages/domain/entities/HostedSessionConfig';
import { StepId } from '@/packages/domain/value-objects/StepId';
import { AuthenticationState } from '@/packages/domain/value-objects/AuthenticationState';
import { ElectronCheckoutConfirmationAdapter } from '@/packages/infrastructure/adapters/ipc/ElectronCheckoutConfirmationAdapter';
let progressMonitorInterval: NodeJS.Timeout | null = null;
@@ -14,6 +15,10 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
const automationEngine = container.getAutomationEngine();
const logger = container.getLogger();
// Setup checkout confirmation adapter and wire it into the container
const checkoutConfirmationAdapter = new ElectronCheckoutConfirmationAdapter(mainWindow);
container.setConfirmCheckoutUseCase(checkoutConfirmationAdapter);
// Authentication handlers
ipcMain.handle('auth:check', async () => {
try {
@@ -21,11 +26,10 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
const checkAuthUseCase = container.getCheckAuthenticationUseCase();
if (!checkAuthUseCase) {
logger.warn('Authentication not available in mock mode');
logger.error('Authentication use case not available');
return {
success: true,
state: AuthenticationState.AUTHENTICATED,
message: 'Mock mode - authentication bypassed'
success: false,
error: 'Authentication not available - check system configuration'
};
}
@@ -301,4 +305,36 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
};
}
});
// Browser mode control handlers
ipcMain.handle('browser-mode:get', async () => {
try {
const loader = container.getBrowserModeConfigLoader();
if (process.env.NODE_ENV === 'development') {
return { mode: loader.getDevelopmentMode(), isDevelopment: true };
}
return { mode: 'headless', isDevelopment: false };
} catch (error) {
const err = error instanceof Error ? error : new Error('Unknown error');
logger.error('Failed to get browser mode', err);
return { mode: 'headless', isDevelopment: false };
}
});
ipcMain.handle('browser-mode:set', async (_event: IpcMainInvokeEvent, mode: 'headed' | 'headless') => {
try {
if (process.env.NODE_ENV === 'development') {
const loader = container.getBrowserModeConfigLoader();
loader.setDevelopmentMode(mode);
logger.info('Browser mode updated', { mode });
return { success: true, mode };
}
logger.warn('Browser mode change requested but not in development mode');
return { success: false, error: 'Only available in development mode' };
} catch (error) {
const err = error instanceof Error ? error : new Error('Unknown error');
logger.error('Failed to set browser mode', err);
return { success: false, error: err.message };
}
});
}

View File

@@ -20,6 +20,17 @@ export interface AuthActionResponse {
error?: string;
}
export interface CheckoutConfirmationRequest {
price: string;
state: 'ready' | 'insufficient_funds';
sessionMetadata: {
sessionName: string;
trackId: string;
carIds: string[];
};
timeoutMs: number;
}
export interface ElectronAPI {
startAutomation: (config: HostedSessionConfig) => Promise<{
success: boolean;
@@ -37,6 +48,12 @@ export interface ElectronAPI {
initiateLogin: () => Promise<AuthActionResponse>;
confirmLogin: () => Promise<AuthActionResponse>;
logout: () => Promise<AuthActionResponse>;
// Browser Mode APIs
getBrowserMode: () => Promise<{ mode: 'headed' | 'headless'; isDevelopment: boolean }>;
setBrowserMode: (mode: 'headed' | 'headless') => Promise<{ success: boolean; mode?: string; error?: string }>;
// Checkout Confirmation APIs
onCheckoutConfirmationRequest: (callback: (request: CheckoutConfirmationRequest) => void) => () => void;
confirmCheckout: (decision: 'confirmed' | 'cancelled' | 'timeout') => void;
}
contextBridge.exposeInMainWorld('electronAPI', {
@@ -56,4 +73,18 @@ contextBridge.exposeInMainWorld('electronAPI', {
initiateLogin: () => ipcRenderer.invoke('auth:login'),
confirmLogin: () => ipcRenderer.invoke('auth:confirmLogin'),
logout: () => ipcRenderer.invoke('auth:logout'),
// Browser Mode APIs
getBrowserMode: () => ipcRenderer.invoke('browser-mode:get'),
setBrowserMode: (mode: 'headed' | 'headless') => ipcRenderer.invoke('browser-mode:set', mode),
// Checkout Confirmation APIs
onCheckoutConfirmationRequest: (callback: (request: CheckoutConfirmationRequest) => void) => {
const listener = (_event: any, request: CheckoutConfirmationRequest) => callback(request);
ipcRenderer.on('checkout:request-confirmation', listener);
return () => {
ipcRenderer.removeListener('checkout:request-confirmation', listener);
};
},
confirmCheckout: (decision: 'confirmed' | 'cancelled' | 'timeout') => {
ipcRenderer.send('checkout:confirm', decision);
},
} as ElectronAPI);