website refactor
This commit is contained in:
@@ -1,32 +1,32 @@
|
||||
import type { Browser, Page, BrowserContext } from 'playwright';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
|
||||
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
|
||||
import { CheckoutPrice } from '../../../../domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '../../../../domain/value-objects/CheckoutState';
|
||||
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
|
||||
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
|
||||
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
|
||||
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
|
||||
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
|
||||
import type { Browser, BrowserContext, Page } from 'playwright';
|
||||
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
|
||||
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
|
||||
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
|
||||
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
|
||||
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
|
||||
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
|
||||
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
|
||||
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import { IRACING_SELECTORS, IRACING_URLS, IRACING_TIMEOUTS, BLOCKED_KEYWORDS } from '../dom/RacingSelectors';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
|
||||
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
|
||||
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
|
||||
import { CheckoutPrice } from '../../../../domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '../../../../domain/value-objects/CheckoutState';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import { SessionCookieStore } from '../auth/SessionCookieStore';
|
||||
import { BLOCKED_KEYWORDS, IRACING_SELECTORS, IRACING_TIMEOUTS, IRACING_URLS } from '../dom/RacingSelectors';
|
||||
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
|
||||
|
||||
import { PageStateValidator, PageStateValidation, PageStateValidationResult } from 'apps/companion/main/automation/domain/services/PageStateValidator';
|
||||
import { IRacingDomNavigator } from '../dom/RacingDomNavigator';
|
||||
import { SafeClickService } from '../dom/SafeClickService';
|
||||
import { IRacingDomInteractor } from '../dom/RacingDomInteractor';
|
||||
import { PageStateValidation, PageStateValidationResult, PageStateValidator } from 'apps/companion/main/automation/domain/services/PageStateValidator';
|
||||
import { PlaywrightAuthSessionService } from '../auth/PlaywrightAuthSessionService';
|
||||
import { IRacingPlaywrightAuthFlow } from '../auth/RacingPlaywrightAuthFlow';
|
||||
import { IRacingDomInteractor } from '../dom/RacingDomInteractor';
|
||||
import { IRacingDomNavigator } from '../dom/RacingDomNavigator';
|
||||
import { SafeClickService } from '../dom/SafeClickService';
|
||||
import { WizardStepOrchestrator } from './WizardStepOrchestrator';
|
||||
|
||||
|
||||
@@ -425,7 +425,6 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
private config: Required<PlaywrightConfig>;
|
||||
private browserSession: PlaywrightBrowserSession;
|
||||
private connected = false;
|
||||
private isConnecting = false;
|
||||
private logger: LoggerPort | undefined;
|
||||
private cookieStore: SessionCookieStore;
|
||||
private authService: PlaywrightAuthSessionService;
|
||||
@@ -539,8 +538,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
* Minimal attachPanel helper for tests that simulates deterministic lifecycle events.
|
||||
* Emits 'panel-attached' and then 'action-started' immediately for deterministic tests.
|
||||
*/
|
||||
async attachPanel(page?: Page, actionId?: string): Promise<void> {
|
||||
const selector = '#gridpilot-overlay'
|
||||
async attachPanel(actionId?: string): Promise<void> {
|
||||
await this.emitLifecycle({ type: 'panel-attached', actionId, timestamp: Date.now(), payload: { _selector } })
|
||||
await this.emitLifecycle({ type: 'action-started', actionId, timestamp: Date.now() })
|
||||
}
|
||||
@@ -659,32 +657,6 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
this.syncSessionStateFromBrowser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up stale SingletonLock file if it exists and the owning process is not running.
|
||||
* On Unix systems, SingletonLock is a symbolic link pointing to a socket file.
|
||||
* If the browser crashed or was force quit, this file remains and blocks new launches.
|
||||
*/
|
||||
private async cleanupStaleLockFile(userDataDir: string): Promise<void> {
|
||||
const singletonLockPath = path.join(userDataDir, 'SingletonLock');
|
||||
|
||||
try {
|
||||
// Check if lock file exists
|
||||
if (!fs.existsSync(singletonLockPath)) {
|
||||
return; // No lock file, we're good
|
||||
}
|
||||
|
||||
this.log('info', 'Found existing SingletonLock, attempting cleanup', { path: singletonLockPath });
|
||||
|
||||
// Try to remove the lock file
|
||||
// On Unix, SingletonLock is typically a symlink, so unlink works for both files and symlinks
|
||||
fs.unlinkSync(singletonLockPath);
|
||||
this.log('info', 'Cleaned up stale SingletonLock file');
|
||||
} catch (error) {
|
||||
// If we can't remove it, the browser might actually be running
|
||||
// Log warning but continue - Playwright will give us a proper error if it's actually locked
|
||||
this.log('warn', 'Could not clean up SingletonLock', { error: String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
await this.browserSession.disconnect();
|
||||
@@ -827,90 +799,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
17: 'trackConditions',
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect which wizard page is currently displayed by checking container existence.
|
||||
* Returns the page name (e.g., 'cars', 'track') or null if no page is detected.
|
||||
*
|
||||
* This method checks each step container from IRACING_SELECTORS.wizard.stepContainers
|
||||
* and returns the first one that exists in the DOM.
|
||||
*
|
||||
* @returns Page name or null if unknown
|
||||
*/
|
||||
private async detectCurrentWizardPage(): Promise<string | null> {
|
||||
if (!this.page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check each container in stepContainers map
|
||||
const containers = IRACING_SELECTORS.wizard.stepContainers;
|
||||
|
||||
for (const [pageName, selector] of Object.entries(containers)) {
|
||||
const count = await this.page.locator(selector).count();
|
||||
if (count > 0) {
|
||||
this.log('debug', 'Detected wizard page', { pageName, selector });
|
||||
return pageName;
|
||||
}
|
||||
}
|
||||
|
||||
// No container found
|
||||
this.log('debug', 'No wizard page detected');
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.log('debug', 'Error detecting wizard page', { error: String(error) });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize step counter with actual wizard state.
|
||||
* Calculates the skip offset when wizard auto-skips steps (e.g., 8→11).
|
||||
*
|
||||
* @param expectedStep The step number we're trying to execute
|
||||
* @param actualPage The actual wizard page detected (from detectCurrentWizardPage)
|
||||
* @returns Skip offset (0 if no skip, >0 if steps were skipped)
|
||||
*/
|
||||
private synchronizeStepCounter(expectedStep: number, actualPage: string | null): number {
|
||||
if (!actualPage) {
|
||||
return 0; // Unknown state, no skip
|
||||
}
|
||||
|
||||
// Find which step number corresponds to the actual page
|
||||
let actualStep: number | null = null;
|
||||
for (const [step, pageName] of Object.entries(PlaywrightAutomationAdapter.STEP_TO_PAGE_MAP)) {
|
||||
if (pageName === actualPage) {
|
||||
actualStep = parseInt(step, 10);
|
||||
break; // Use first match
|
||||
}
|
||||
}
|
||||
|
||||
if (actualStep === null) {
|
||||
return 0; // Unknown page, no skip
|
||||
}
|
||||
|
||||
// Calculate skip offset
|
||||
const skipOffset = actualStep - expectedStep;
|
||||
|
||||
if (skipOffset > 0) {
|
||||
// Wizard skipped ahead - log warning with skipped step numbers
|
||||
const skippedSteps: number[] = [];
|
||||
for (let i = expectedStep; i < actualStep; i++) {
|
||||
skippedSteps.push(i);
|
||||
}
|
||||
|
||||
this.log('warn', 'Wizard auto-skip detected', {
|
||||
expectedStep,
|
||||
actualStep,
|
||||
skipOffset,
|
||||
skippedSteps,
|
||||
});
|
||||
|
||||
return skipOffset;
|
||||
}
|
||||
|
||||
// No skip or backward navigation
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save debug information (screenshot and HTML) when a step fails.
|
||||
@@ -1509,427 +1398,16 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select weather type via Chakra UI radio button.
|
||||
* iRacing's modern UI uses a radio group with options:
|
||||
* - "Static Weather" (value: 2, checked by default)
|
||||
* - "Forecasted weather" (value: 1)
|
||||
* - "Timeline editor" (value: 3)
|
||||
*
|
||||
* @param weatherType The weather type to select (e.g., "static", "forecasted", "timeline", or the value)
|
||||
*/
|
||||
private async selectWeatherType(weatherType: string): Promise<void> {
|
||||
if (!this.page) {
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('info', 'Selecting weather type via radio button', { weatherType });
|
||||
|
||||
// Map common weather type names to selectors
|
||||
const weatherTypeLower = weatherType.toLowerCase();
|
||||
let labelSelector: string;
|
||||
|
||||
if (weatherTypeLower.includes('static') || weatherType === '2') {
|
||||
labelSelector = 'label.chakra-radio:has-text("Static Weather")';
|
||||
} else if (weatherTypeLower.includes('forecast') || weatherType === '1') {
|
||||
labelSelector = 'label.chakra-radio:has-text("Forecasted weather")';
|
||||
} else if (weatherTypeLower.includes('timeline') || weatherTypeLower.includes('custom') || weatherType === '3') {
|
||||
labelSelector = 'label.chakra-radio:has-text("Timeline editor")';
|
||||
} else {
|
||||
// Default to static weather
|
||||
labelSelector = 'label.chakra-radio:has-text("Static Weather")';
|
||||
this.log('warn', `Unknown weather type "${weatherType}", defaulting to Static Weather`);
|
||||
}
|
||||
|
||||
// Check if radio group exists (weather step might be optional)
|
||||
const radioGroup = this.page.locator('[role="radiogroup"]').first();
|
||||
const exists = await radioGroup.count() > 0;
|
||||
|
||||
if (!exists) {
|
||||
this.log('debug', 'Weather radio group not found, step may be optional');
|
||||
return;
|
||||
}
|
||||
|
||||
// Click the radio button label
|
||||
const radioLabel = this.page.locator(labelSelector).first();
|
||||
const isVisible = await radioLabel.isVisible().catch(() => false);
|
||||
|
||||
if (isVisible) {
|
||||
await radioLabel.click({ timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('info', 'Selected weather type', { weatherType, _selector: labelSelector });
|
||||
} else {
|
||||
this.log('debug', 'Weather type radio not visible, may already be selected or step is different');
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('warn', 'Could not select weather type (non-critical)', { error: message, weatherType });
|
||||
// Don't throw - weather type selection is optional
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the "Add Admin" modal is currently visible.
|
||||
* This modal appears when the user clicks "Add Admin" on the Admins step.
|
||||
* @returns true if the admin modal is visible, false otherwise
|
||||
*/
|
||||
private async isAdminModalVisible(): Promise<boolean> {
|
||||
if (!this.page) return false;
|
||||
|
||||
try {
|
||||
// Look for a modal with admin-related content
|
||||
// The admin modal should have a search input and be separate from the main wizard modal
|
||||
const adminModalSelector = '#set-admins .modal, .modal:has(input[placeholder*="Search"]):has-text("Admin")';
|
||||
const isVisible = await this.page.locator(adminModalSelector).first().isVisible().catch(() => false);
|
||||
return isVisible;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the "Add Car" modal is currently visible.
|
||||
* This modal appears when the user clicks "Add Car" on the Cars step.
|
||||
* @returns true if the car modal is visible, false otherwise
|
||||
*/
|
||||
private async isCarModalVisible(): Promise<boolean> {
|
||||
if (!this.page) return false;
|
||||
|
||||
try {
|
||||
// Look for a modal with car-related content
|
||||
// The car modal should have a search input and be part of the set-cars step
|
||||
const carModalSelector = '#set-cars .modal, .modal:has(input[placeholder*=\"Search\"]):has-text(\"Car\")';
|
||||
const isVisible = await this.page.locator(carModalSelector).first().isVisible().catch(() => false);
|
||||
return isVisible;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the "Add Track" modal is currently visible.
|
||||
* This modal appears when the user clicks "Add Track" on the Track step.
|
||||
* @returns true if the track modal is visible, false otherwise
|
||||
*/
|
||||
private async isTrackModalVisible(): Promise<boolean> {
|
||||
if (!this.page) return false;
|
||||
|
||||
try {
|
||||
// Look for a modal with track-related content
|
||||
// The track modal should have a search input and be part of the set-track step
|
||||
const trackModalSelector = '#set-track .modal, .modal:has(input[placeholder*=\"Search\"]):has-text(\"Track\")';
|
||||
const isVisible = await this.page.locator(trackModalSelector).first().isVisible().catch(() => false);
|
||||
return isVisible;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the "Add Car" button to open the Add Car modal.
|
||||
* This button is located on the Set Cars step (Step 8).
|
||||
*/
|
||||
private async clickAddCarButton(): Promise<void> {
|
||||
if (!this.page) {
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
const addCarButtonSelector = this.isRealMode()
|
||||
? IRACING_SELECTORS.steps.addCarButton
|
||||
: '[data-action="add-car"]';
|
||||
|
||||
try {
|
||||
this.log('info', 'Clicking Add Car button to open modal');
|
||||
// Use 'attached' instead of 'visible' because iRacing wizard steps have class="hidden"
|
||||
await this.page.waitForSelector(addCarButtonSelector, {
|
||||
state: 'attached',
|
||||
timeout: this.isRealMode() ? IRACING_TIMEOUTS.elementWait : this.config.timeout,
|
||||
});
|
||||
await this.safeClick(addCarButtonSelector);
|
||||
this.log('info', 'Clicked Add Car button');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('error', 'Could not click Add Car button', { error: message });
|
||||
throw new Error(`Failed to click Add Car button: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the Add Car modal to appear.
|
||||
*/
|
||||
private async waitForAddCarModal(): Promise<void> {
|
||||
if (!this.page) {
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('debug', 'Waiting for Add Car modal to appear (primary selector)');
|
||||
// Wait for modal container - expanded selector list to tolerate UI variants
|
||||
const modalSelector = IRACING_SELECTORS.steps.addCarModal;
|
||||
await this.page.waitForSelector(modalSelector, {
|
||||
state: 'attached',
|
||||
timeout: this.isRealMode() ? IRACING_TIMEOUTS.elementWait : this.config.timeout,
|
||||
});
|
||||
// Brief pause for modal animation (reduced from 300ms)
|
||||
await this.page.waitForTimeout(150);
|
||||
this.log('info', 'Add Car modal is visible', { _selector: modalSelector });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('warn', 'Add Car modal not found with primary _selector, dumping #create-race-wizard innerHTML and retrying', { error: message });
|
||||
const html = await this.page!.innerHTML('#create-race-wizard').catch(() => '');
|
||||
this.log('debug', 'create-race-wizard innerHTML (truncated)', { html: html ? html.slice(0, 2000) : '' });
|
||||
this.log('info', 'Retrying wait for Add Car modal with extended timeout');
|
||||
try {
|
||||
const modalSelectorRetry = IRACING_SELECTORS.steps.addCarModal;
|
||||
await this.page.waitForSelector(modalSelectorRetry, {
|
||||
state: 'attached',
|
||||
timeout: 10000,
|
||||
});
|
||||
await this.page.waitForTimeout(150);
|
||||
this.log('info', 'Add Car modal found after retry', { _selector: modalSelectorRetry });
|
||||
} catch {
|
||||
this.log('warn', 'Add Car modal still not found after retry');
|
||||
}
|
||||
// Don't throw - modal might appear differently in real iRacing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the "Add Track" / "Select Track" button to open the Add Track modal.
|
||||
* This button is located on the Set Track step (Step 11).
|
||||
*/
|
||||
private async clickAddTrackButton(): Promise<void> {
|
||||
if (!this.page) {
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
const addTrackButtonSelector = IRACING_SELECTORS.steps.addTrackButton;
|
||||
|
||||
try {
|
||||
this.log('info', 'Clicking Add Track button to open modal');
|
||||
// Use 'attached' instead of 'visible' because iRacing wizard steps have class="hidden"
|
||||
await this.page.waitForSelector(addTrackButtonSelector, {
|
||||
state: 'attached',
|
||||
timeout: this.isRealMode() ? IRACING_TIMEOUTS.elementWait : this.config.timeout,
|
||||
});
|
||||
await this.safeClick(addTrackButtonSelector);
|
||||
this.log('info', 'Clicked Add Track button');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('error', 'Could not click Add Track button', { error: message });
|
||||
throw new Error(`Failed to click Add Track button: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the Add Track modal to appear.
|
||||
*/
|
||||
private async waitForAddTrackModal(): Promise<void> {
|
||||
if (!this.page) {
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('debug', 'Waiting for Add Track modal to appear');
|
||||
// Wait for modal container - use 'attached' because iRacing wizard steps have class="hidden"
|
||||
const modalSelector = IRACING_SELECTORS.steps.addTrackModal;
|
||||
await this.page.waitForSelector(modalSelector, {
|
||||
state: 'attached',
|
||||
timeout: this.isRealMode() ? IRACING_TIMEOUTS.elementWait : this.config.timeout,
|
||||
});
|
||||
// Brief pause for modal animation (reduced from 300ms)
|
||||
await this.page.waitForTimeout(150);
|
||||
this.log('info', 'Add Track modal is visible');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('warn', 'Add Track modal did not appear', { error: message });
|
||||
// Don't throw - modal might appear differently in real iRacing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the first search result in the current modal by clicking its "Select" button.
|
||||
* In iRacing's Add Car/Track modals, search results are displayed in a table,
|
||||
* and each row has a "Select" button (a.btn.btn-block.btn-primary.btn-xs).
|
||||
*
|
||||
* Two button patterns exist:
|
||||
* 1. Direct select (single-config tracks): a.btn.btn-primary.btn-xs:not(.dropdown-toggle)
|
||||
* 2. Dropdown (multi-config tracks): a.btn.btn-primary.btn-xs.dropdown-toggle → opens menu → click .dropdown-item
|
||||
*
|
||||
* Clicking "Select" immediately adds the item - there is no confirm step.
|
||||
*/
|
||||
private async selectFirstSearchResult(): Promise<void> {
|
||||
if (!this.page) {
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
// First try direct select button (non-dropdown) - using verified selectors
|
||||
// Try both track and car select buttons as this method is shared
|
||||
const directSelectors = [
|
||||
IRACING_SELECTORS.steps.trackSelectButton,
|
||||
IRACING_SELECTORS.steps.carSelectButton
|
||||
];
|
||||
|
||||
for (const selector of directSelectors) {
|
||||
const button = this.page.locator(selector).first();
|
||||
if (await button.count() > 0 && await button.isVisible()) {
|
||||
await this.safeClick(_selector, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('info', 'Clicked direct Select button for first search result', { selector });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: dropdown toggle pattern (for multi-config tracks)
|
||||
const dropdownSelector = IRACING_SELECTORS.steps.trackSelectDropdown;
|
||||
const dropdownButton = this.page.locator(dropdownSelector).first();
|
||||
|
||||
if (await dropdownButton.count() > 0 && await dropdownButton.isVisible()) {
|
||||
// Click dropdown to open menu
|
||||
await this.safeClick(dropdownSelector, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('debug', 'Clicked dropdown toggle, waiting for menu', { _selector: dropdownSelector });
|
||||
|
||||
// Wait for dropdown menu to appear
|
||||
await this.page.waitForSelector('.dropdown-menu.show', { timeout: 3000 }).catch(() => { });
|
||||
|
||||
// Click first item in dropdown (first track config)
|
||||
const itemSelector = IRACING_SELECTORS.steps.trackSelectDropdownItem;
|
||||
await this.page.waitForTimeout(200);
|
||||
await this.safeClick(itemSelector, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('info', 'Clicked first dropdown item to select track config', { _selector: itemSelector });
|
||||
return;
|
||||
}
|
||||
|
||||
// Final fallback: try tolerant car row selectors (various UI variants)
|
||||
const carRowSelector = '.car-row, .car-item, [data-testid*="car"], [id*="favorite_cars"], [id*="select-car"]';
|
||||
const carRow = this.page.locator(carRowSelector).first();
|
||||
if (await carRow.count() > 0) {
|
||||
this.log('info', 'Fallback: clicking car row/item to select', { _selector: carRowSelector });
|
||||
// Click the row itself (or its first clickable descendant)
|
||||
try {
|
||||
await this.safeClick(carRowSelector, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('info', 'Clicked car row fallback selector');
|
||||
return;
|
||||
} catch (e) {
|
||||
this.log('debug', 'Car row fallback click failed, attempting to click first link inside row', { error: String(e) });
|
||||
const linkInside = this.page.locator(`${carRowSelector} a, ${carRowSelector} button`).first();
|
||||
if (await linkInside.count() > 0 && await linkInside.isVisible()) {
|
||||
await this.safeClick(`${carRowSelector} a, ${carRowSelector} button`, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('info', 'Clicked link/button inside car row fallback');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none found, throw error
|
||||
throw new Error('No Select button found in modal table and no fallback car row found');
|
||||
}
|
||||
|
||||
// NOTE: clickCarModalConfirm() and clickTrackModalConfirm() have been removed.
|
||||
// The Add Car/Track modals use a table with "Select" buttons that immediately add the item.
|
||||
// There is no separate confirm step - clicking "Select" closes the modal and adds the car/track.
|
||||
// The selectFirstSearchResult() method now handles clicking the "Select" button directly.
|
||||
|
||||
/**
|
||||
* Click the confirm/select button in the "Add Admin" modal.
|
||||
* Uses a specific selector that avoids the checkout button.
|
||||
*/
|
||||
private async clickAdminModalConfirm(): Promise<void> {
|
||||
if (!this.page) {
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
// Use a selector specific to the admin modal, NOT the main wizard modal footer
|
||||
// The admin modal confirm button should be inside the admin modal content
|
||||
const adminConfirmSelector = '#set-admins .modal .btn-primary, #set-admins .modal button:has-text("Add"), #set-admins .modal button:has-text("Select")';
|
||||
|
||||
try {
|
||||
// Use 'attached' instead of 'visible' because iRacing wizard steps have class="hidden"
|
||||
await this.page.waitForSelector(adminConfirmSelector, {
|
||||
state: 'attached',
|
||||
timeout: IRACING_TIMEOUTS.elementWait,
|
||||
});
|
||||
await this.safeClick(adminConfirmSelector, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('info', 'Clicked admin modal confirm button');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('warn', 'Could not click admin modal confirm button', { error: message });
|
||||
throw new Error(`Failed to confirm admin selection: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the "New Race" option in the modal that appears after clicking "Create a Race".
|
||||
* Supports both:
|
||||
* - Direct "New Race" button
|
||||
* - Dropdown menu with "Last Settings" / "New Race" items (fixture HTML)
|
||||
*/
|
||||
private async clickNewRaceInModal(): Promise<void> {
|
||||
if (!this.page) {
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('info', 'Waiting for Create Race modal to appear');
|
||||
|
||||
const modalSelector = IRACING_SELECTORS.hostedRacing.createRaceModal;
|
||||
await this.page.waitForSelector(modalSelector, {
|
||||
state: 'attached',
|
||||
timeout: IRACING_TIMEOUTS.elementWait,
|
||||
});
|
||||
|
||||
this.log('info', 'Create Race modal attached, resolving New Race control');
|
||||
|
||||
const directSelector = IRACING_SELECTORS.hostedRacing.newRaceButton;
|
||||
const direct = this.page.locator(directSelector).first();
|
||||
const hasDirect =
|
||||
(await direct.count().catch(() => 0)) > 0 &&
|
||||
(await direct.isVisible().catch(() => false));
|
||||
|
||||
if (hasDirect) {
|
||||
this.log('info', 'Clicking direct New Race button', { _selector: directSelector });
|
||||
await this.safeClick(directSelector, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
} else {
|
||||
const dropdownToggleSelector =
|
||||
'.btn-toolbar .btn-group.dropup > a.dropdown-toggle, .btn-group.dropup > a.dropdown-toggle';
|
||||
const dropdownToggle = this.page.locator(dropdownToggleSelector).first();
|
||||
const hasDropdown =
|
||||
(await dropdownToggle.count().catch(() => 0)) > 0 &&
|
||||
(await dropdownToggle.isVisible().catch(() => false));
|
||||
|
||||
if (!hasDropdown) {
|
||||
throw new Error(
|
||||
`Create Race modal present but no direct New Race button or dropdown toggle found (selectors: ${directSelector}, ${dropdownToggleSelector})`,
|
||||
);
|
||||
}
|
||||
|
||||
this.log('info', 'Clicking dropdown toggle to open New Race menu', {
|
||||
selector: dropdownToggleSelector,
|
||||
});
|
||||
await this.safeClick(dropdownToggleSelector, {
|
||||
timeout: IRACING_TIMEOUTS.elementWait,
|
||||
});
|
||||
|
||||
const menuSelector =
|
||||
'.dropdown-menu a.dropdown-item.text-danger:has-text("New Race"), .dropdown-menu a.dropdown-item:has-text("New Race")';
|
||||
this.log('debug', 'Waiting for New Race entry in dropdown menu', {
|
||||
selector: menuSelector,
|
||||
});
|
||||
await this.page.waitForSelector(menuSelector, {
|
||||
state: 'attached',
|
||||
timeout: IRACING_TIMEOUTS.elementWait,
|
||||
});
|
||||
await this.safeClick(menuSelector, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('info', 'Clicked New Race dropdown item');
|
||||
}
|
||||
|
||||
this.log('info', 'Waiting for Race Information form to load after New Race selection');
|
||||
await this.page.waitForTimeout(500);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('error', 'Failed to click New Race in modal', { error: message });
|
||||
throw new Error(`Failed to click New Race button: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login for real iRacing website.
|
||||
@@ -2084,143 +1562,8 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
await this.navigator.waitForStep(stepNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific wizard step to be visible in real mode.
|
||||
* Uses the step container IDs from IRACING_SELECTORS.wizard.stepContainers
|
||||
*/
|
||||
private async waitForWizardStep(stepName: keyof typeof IRACING_SELECTORS.wizard.stepContainers): Promise<void> {
|
||||
if (!this.page || !this.isRealMode()) return;
|
||||
|
||||
const containerSelector = IRACING_SELECTORS.wizard.stepContainers[stepName];
|
||||
if (!containerSelector) {
|
||||
this.log('warn', `Unknown wizard step: ${stepName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('debug', `Waiting for wizard step: ${stepName}`, { _selector: containerSelector });
|
||||
// Use 'attached' instead of 'visible' because iRacing wizard steps are marked as
|
||||
// 'active hidden' in the DOM - they exist but are hidden via CSS class
|
||||
await this.page.waitForSelector(containerSelector, {
|
||||
state: 'attached',
|
||||
timeout: 15000,
|
||||
});
|
||||
// Brief pause to ensure DOM is settled
|
||||
await this.page.waitForTimeout(100);
|
||||
} catch (error) {
|
||||
this.log('warn', `Wizard step not attached: ${stepName}`, { error: String(error) });
|
||||
// Don't throw - step might be combined with another or skipped
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a form field with fallback selector support.
|
||||
* Tries the primary selector first, then falls back to alternative selectors.
|
||||
* This is needed because iRacing's form structure can vary slightly.
|
||||
*/
|
||||
private async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResultDTO> {
|
||||
if (!this.page) {
|
||||
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
|
||||
}
|
||||
|
||||
const selector = this.getFieldSelector(fieldName);
|
||||
const timeout = this.isRealMode() ? IRACING_TIMEOUTS.elementWait : this.config.timeout;
|
||||
|
||||
// Split combined selectors and try each one
|
||||
const selectors = _selector.split(', ').map(s => s.trim());
|
||||
|
||||
for (const sel of selectors) {
|
||||
try {
|
||||
this.log('debug', `Trying _selector for ${fieldName}`, { _selector: sel });
|
||||
|
||||
// Check if element exists and is visible
|
||||
const element = this.page.locator(sel).first();
|
||||
const isVisible = await element.isVisible().catch(() => false);
|
||||
|
||||
if (isVisible) {
|
||||
// Use 'attached' instead of 'visible' because iRacing wizard steps have class="hidden"
|
||||
await element.waitFor({ state: 'attached', timeout });
|
||||
await element.fill(value);
|
||||
this.log('info', `Successfully filled ${fieldName}`, { _selector: sel, value });
|
||||
return { success: true, fieldName, valueSet: value };
|
||||
}
|
||||
} catch (error) {
|
||||
this.log('debug', `Selector failed for ${fieldName}`, { _selector: sel, error: String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
// If none worked, try the original combined selector (Playwright handles comma-separated)
|
||||
try {
|
||||
this.log('debug', `Trying combined selector for ${fieldName}`, { selector });
|
||||
// Use 'attached' instead of 'visible' because iRacing wizard steps have class="hidden"
|
||||
await this.page.waitForSelector(_selector, { state: 'attached', timeout });
|
||||
await this.page.fill(selector, value);
|
||||
return { success: true, fieldName, valueSet: value };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('error', `Failed to fill ${fieldName}`, { _selector, error: message });
|
||||
return { success: false, fieldName, valueSet: value, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the "Next" button in the wizard footer.
|
||||
* In real iRacing, the next button text shows the next step name (e.g., "Server Details").
|
||||
* @param nextStepName The name of the next step (for logging and fallback)
|
||||
*/
|
||||
private async clickNextButton(nextStepName: string): Promise<void> {
|
||||
if (!this.page) {
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
if (!this.isRealMode()) {
|
||||
// Mock mode uses data-action="next"
|
||||
await this.clickAction('next');
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = IRACING_TIMEOUTS.elementWait;
|
||||
|
||||
// Primary: Look for the next button with caret icon (it points to next step)
|
||||
const nextButtonSelector = IRACING_SELECTORS.wizard.nextButton;
|
||||
|
||||
// Fallback: Look for button with the next step name
|
||||
const fallbackSelector = `.wizard-footer a.btn:has-text("${nextStepName}")`;
|
||||
|
||||
try {
|
||||
// Attempt primary selector first using a forced safe click.
|
||||
// Some wizard footer buttons are present/attached but not considered "visible" by Playwright
|
||||
// (offscreen, overlapped by overlays, or transitional). Use a forced safe click first,
|
||||
// then fall back to name-based or last-resort selectors if that fails.
|
||||
this.log('debug', 'Attempting next button (primary) with forced click', { _selector: nextButtonSelector });
|
||||
try {
|
||||
await this.safeClick(nextButtonSelector, { timeout, force: true });
|
||||
this.log('info', `Clicked next button to ${nextStepName} (primary forced)`);
|
||||
return;
|
||||
} catch (e) {
|
||||
this.log('debug', 'Primary forced click failed, falling back', { error: String(e) });
|
||||
}
|
||||
|
||||
// Try fallback with step name (also attempt forced click)
|
||||
this.log('debug', 'Trying fallback next button (forced)', { _selector: fallbackSelector });
|
||||
try {
|
||||
await this.safeClick(fallbackSelector, { timeout, force: true });
|
||||
this.log('info', `Clicked next button (fallback) to ${nextStepName}`);
|
||||
return;
|
||||
} catch (e) {
|
||||
this.log('debug', 'Fallback forced click failed, trying last resort', { error: String(e) });
|
||||
}
|
||||
|
||||
// Last resort: any non-disabled button in wizard footer (use forced click)
|
||||
const lastResort = '.wizard-footer a.btn:not(.disabled):last-child';
|
||||
await this.safeClick(lastResort, { timeout, force: true });
|
||||
this.log('info', `Clicked next button (last resort) to ${nextStepName}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('error', `Failed to click next button to ${nextStepName}`, { error: message });
|
||||
throw new Error(`Failed to navigate to ${nextStepName}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async clickAction(action: string): Promise<ClickResultDTO> {
|
||||
if (!this.page) {
|
||||
@@ -2837,66 +2180,6 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
return this.authService.getLoginUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for login success by monitoring the page URL.
|
||||
* Login is successful when:
|
||||
* - URL contains 'members.iracing.com' AND
|
||||
* - URL does NOT contain 'oauth.iracing.com' (login page)
|
||||
*
|
||||
* @param timeoutMs Maximum time to wait for login (default: 5 minutes)
|
||||
* @returns true if login was detected, false if timeout
|
||||
*/
|
||||
private async waitForLoginSuccess(timeoutMs = 300000): Promise<boolean> {
|
||||
if (!this.page) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
this.log('info', 'Waiting for login success', { timeoutMs });
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
try {
|
||||
const url = this.page.url();
|
||||
|
||||
// Success: User is on members site (not oauth login page)
|
||||
// Check for various success indicators:
|
||||
// - URL contains members.iracing.com but not oauth.iracing.com
|
||||
// - Or URL is the hosted sessions page
|
||||
const isOnMembersSite = url.includes('members.iracing.com');
|
||||
const isOnLoginPage = url.includes('oauth.iracing.com') ||
|
||||
url.includes('/membersite/login') ||
|
||||
url.includes('/login.jsp');
|
||||
|
||||
if (isOnMembersSite && !isOnLoginPage) {
|
||||
this.log('info', 'Login success detected', { url });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if page is closed (user closed the browser)
|
||||
if (this.page.isClosed()) {
|
||||
this.log('warn', 'Browser page was closed by user');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait before checking again
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
} catch (error) {
|
||||
// Page might be navigating or closed
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.log('debug', 'Error checking URL during login wait', { error: message });
|
||||
|
||||
// If we can't access the page, it might be closed
|
||||
if (!this.page || this.page.isClosed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
this.log('warn', 'Login wait timed out');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate login by opening the Playwright browser to the iRacing login page.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Browser, BrowserContext, Page } from 'playwright';
|
||||
import { chromium } from 'playwright-extra';
|
||||
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import type { LoggerPort } from 'apps/companion/main/automation/application/ports/LoggerPort';
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import type { Page } from 'playwright';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
|
||||
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
|
||||
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
|
||||
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
|
||||
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
|
||||
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
|
||||
import { CheckoutPrice } from '../../../../domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '../../../../domain/value-objects/CheckoutState';
|
||||
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import { IRacingDomInteractor } from '../dom/RacingDomInteractor';
|
||||
import { IRacingDomNavigator } from '../dom/RacingDomNavigator';
|
||||
import { IRACING_SELECTORS } from '../dom/RacingSelectors';
|
||||
import type { PlaywrightConfig } from './PlaywrightAutomationAdapter';
|
||||
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
|
||||
import { IRacingDomNavigator } from '../dom/RacingDomNavigator';
|
||||
import { IRacingDomInteractor } from '../dom/RacingDomInteractor';
|
||||
import { IRACING_SELECTORS } from '../dom/RacingSelectors';
|
||||
|
||||
import type {
|
||||
PageStateValidation,
|
||||
PageStateValidationResult,
|
||||
} from '../../../../domain/services/PageStateValidator';
|
||||
import type { Result } from '@core/shared/domain/Result';
|
||||
import type {
|
||||
PageStateValidation,
|
||||
PageStateValidationResult,
|
||||
} from '../../../../domain/services/PageStateValidator';
|
||||
|
||||
interface WizardStepOrchestratorDeps {
|
||||
config: Required<PlaywrightConfig>;
|
||||
@@ -65,7 +65,6 @@ export class WizardStepOrchestrator {
|
||||
private readonly browserSession: PlaywrightBrowserSession;
|
||||
private readonly navigator: IRacingDomNavigator;
|
||||
private readonly interactor: IRacingDomInteractor;
|
||||
private readonly authService: AuthenticationServicePort;
|
||||
private readonly logger: LoggerPort | undefined;
|
||||
private readonly totalSteps: number;
|
||||
private readonly getCheckoutConfirmationCallbackInternal: WizardStepOrchestratorDeps['getCheckoutConfirmationCallback'];
|
||||
@@ -79,7 +78,6 @@ export class WizardStepOrchestrator {
|
||||
this.browserSession = deps.browserSession;
|
||||
this.navigator = deps.navigator;
|
||||
this.interactor = deps.interactor;
|
||||
this.authService = deps.authService;
|
||||
this.logger = deps.logger;
|
||||
this.totalSteps = deps.totalSteps;
|
||||
this.getCheckoutConfirmationCallbackInternal =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AdminUser } from '../../domain/entities/AdminUser';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { Email } from '../../domain/value-objects/Email';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { UserRole } from '../../domain/value-objects/UserRole';
|
||||
import { UserStatus } from '../../domain/value-objects/UserStatus';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { ListUsersUseCase } from './ListUsersUseCase';
|
||||
import { AdminUserRepository } from '../ports/AdminUserRepository';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AdminUser } from '../../domain/entities/AdminUser';
|
||||
import { AuthorizationService } from '../../domain/services/AuthorizationService';
|
||||
import { AdminUserRepository } from '../ports/AdminUserRepository';
|
||||
import { ListUsersUseCase } from './ListUsersUseCase';
|
||||
|
||||
// Mock the authorization service
|
||||
vi.mock('../../domain/services/AuthorizationService');
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AdminUserRepository } from '../ports/AdminUserRepository';
|
||||
import type { AdminUser } from '../../domain/entities/AdminUser';
|
||||
import { AuthorizationService } from '../../domain/services/AuthorizationService';
|
||||
import { Email } from '../../domain/value-objects/Email';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { UserRole } from '../../domain/value-objects/UserRole';
|
||||
import { UserStatus } from '../../domain/value-objects/UserStatus';
|
||||
import { Email } from '../../domain/value-objects/Email';
|
||||
import type { AdminUser } from '../../domain/entities/AdminUser';
|
||||
import type { AdminUserRepository } from '../ports/AdminUserRepository';
|
||||
|
||||
export type ListUsersInput = {
|
||||
actorId: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AdminUser } from './AdminUser';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { UserRole } from '../value-objects/UserRole';
|
||||
import { AdminUser } from './AdminUser';
|
||||
|
||||
describe('AdminUser', () => {
|
||||
describe('TDD - Test First', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DomainError, CommonDomainErrorKind } from '@core/shared/errors/DomainError';
|
||||
import type { CommonDomainErrorKind, DomainError } from '@core/shared/errors/DomainError';
|
||||
|
||||
export abstract class AdminDomainError extends Error implements DomainError<CommonDomainErrorKind> {
|
||||
readonly type = 'domain' as const;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AdminUser } from '../entities/AdminUser';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
import { Email } from '../value-objects/Email';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
import { UserRole } from '../value-objects/UserRole';
|
||||
import { UserStatus } from '../value-objects/UserStatus';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AuthorizationService } from './AuthorizationService';
|
||||
import { AdminUser } from '../entities/AdminUser';
|
||||
import { AuthorizationService } from './AuthorizationService';
|
||||
|
||||
describe('AuthorizationService', () => {
|
||||
describe('TDD - Test First', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InMemoryAdminUserRepository } from './InMemoryAdminUserRepository';
|
||||
import { AdminUser } from '../../domain/entities/AdminUser';
|
||||
import { UserRole } from '../../domain/value-objects/UserRole';
|
||||
import { UserStatus } from '../../domain/value-objects/UserStatus';
|
||||
import { InMemoryAdminUserRepository } from './InMemoryAdminUserRepository';
|
||||
|
||||
describe('InMemoryAdminUserRepository', () => {
|
||||
describe('TDD - Test First', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AdminUserRepository, UserFilter, UserListQuery, UserListResult, StoredAdminUser } from '../../domain/repositories/AdminUserRepository';
|
||||
import { AdminUser } from '../../domain/entities/AdminUser';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { AdminUserRepository, StoredAdminUser, UserFilter, UserListQuery, UserListResult } from '../../domain/repositories/AdminUserRepository';
|
||||
import { Email } from '../../domain/value-objects/Email';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
|
||||
/**
|
||||
* In-memory implementation of AdminUserRepository for testing and development
|
||||
|
||||
@@ -2,11 +2,11 @@ import { AdminUser } from '@core/admin/domain/entities/AdminUser';
|
||||
import { AdminUserOrmEntity } from '../entities/AdminUserOrmEntity';
|
||||
import { TypeOrmAdminSchemaError } from '../errors/TypeOrmAdminSchemaError';
|
||||
import {
|
||||
assertNonEmptyString,
|
||||
assertStringArray,
|
||||
assertDate,
|
||||
assertOptionalDate,
|
||||
assertOptionalString,
|
||||
assertDate,
|
||||
assertNonEmptyString,
|
||||
assertOptionalDate,
|
||||
assertOptionalString,
|
||||
assertStringArray,
|
||||
} from '../schema/TypeOrmAdminSchemaGuards';
|
||||
|
||||
export class AdminUserOrmMapper {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { TypeOrmAdminUserRepository } from './TypeOrmAdminUserRepository';
|
||||
import { AdminUserOrmEntity } from '../entities/AdminUserOrmEntity';
|
||||
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
|
||||
import { Email } from '@core/admin/domain/value-objects/Email';
|
||||
import { UserId } from '@core/admin/domain/value-objects/UserId';
|
||||
import { UserRole } from '@core/admin/domain/value-objects/UserRole';
|
||||
import { UserStatus } from '@core/admin/domain/value-objects/UserStatus';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AdminUserOrmEntity } from '../entities/AdminUserOrmEntity';
|
||||
import { TypeOrmAdminUserRepository } from './TypeOrmAdminUserRepository';
|
||||
|
||||
describe('TypeOrmAdminUserRepository', () => {
|
||||
let dataSource: DataSource;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DataSource, Repository } from 'typeorm';
|
||||
import type { AdminUserRepository, UserListQuery, UserListResult, UserFilter, StoredAdminUser } from '@core/admin/domain/repositories/AdminUserRepository';
|
||||
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
|
||||
import { AdminUserOrmEntity } from '../entities/AdminUserOrmEntity';
|
||||
import { AdminUserOrmMapper } from '../mappers/AdminUserOrmMapper';
|
||||
import type { AdminUserRepository, StoredAdminUser, UserFilter, UserListQuery, UserListResult } from '@core/admin/domain/repositories/AdminUserRepository';
|
||||
import { Email } from '@core/admin/domain/value-objects/Email';
|
||||
import { UserId } from '@core/admin/domain/value-objects/UserId';
|
||||
import type { DataSource, Repository } from 'typeorm';
|
||||
import { AdminUserOrmEntity } from '../entities/AdminUserOrmEntity';
|
||||
import { AdminUserOrmMapper } from '../mappers/AdminUserOrmMapper';
|
||||
|
||||
export class TypeOrmAdminUserRepository implements AdminUserRepository {
|
||||
private readonly repository: Repository<AdminUserOrmEntity>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { PageViewRepository } from '../repositories/PageViewRepository';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { describe, expect, it, vi, type Mock, beforeEach } from 'vitest';
|
||||
import type { EntityType } from '../../domain/types/PageView';
|
||||
import { GetEntityAnalyticsQuery, type GetEntityAnalyticsInput } from './GetEntityAnalyticsQuery';
|
||||
import type { PageViewRepository } from '../repositories/PageViewRepository';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import type { EngagementRepository } from '../../domain/repositories/EngagementRepository';
|
||||
import type { EntityType } from '../../domain/types/PageView';
|
||||
import type { PageViewRepository } from '../repositories/PageViewRepository';
|
||||
import { GetEntityAnalyticsQuery, type GetEntityAnalyticsInput } from './GetEntityAnalyticsQuery';
|
||||
|
||||
describe('GetEntityAnalyticsQuery', () => {
|
||||
let pageViewRepository: {
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
* Returns metrics formatted for display to sponsors and admins.
|
||||
*/
|
||||
|
||||
import type { EngagementRepository } from '@core/analytics/domain/repositories/EngagementRepository';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { PageViewRepository } from '../repositories/PageViewRepository';
|
||||
import type { EngagementRepository } from '@core/analytics/domain/repositories/EngagementRepository';
|
||||
import type { EntityType } from '../../domain/types/PageView';
|
||||
import type { SnapshotPeriod } from '../../domain/types/AnalyticsSnapshot';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { SnapshotPeriod } from '../../domain/types/AnalyticsSnapshot';
|
||||
import type { EntityType } from '../../domain/types/PageView';
|
||||
import type { PageViewRepository } from '../repositories/PageViewRepository';
|
||||
|
||||
export interface GetEntityAnalyticsInput {
|
||||
entityType: EntityType;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { describe, expect, it, vi, type Mock, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { EngagementEvent } from '../../domain/entities/EngagementEvent';
|
||||
import type { EngagementRepository } from '../../domain/repositories/EngagementRepository';
|
||||
import type { EngagementAction, EngagementEntityType } from '../../domain/types/EngagementEvent';
|
||||
import { RecordEngagementUseCase, type RecordEngagementInput } from './RecordEngagementUseCase';
|
||||
import type { EngagementRepository } from '../../domain/repositories/EngagementRepository';
|
||||
|
||||
describe('RecordEngagementUseCase', () => {
|
||||
let engagementRepository: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { EngagementEvent } from '../../domain/entities/EngagementEvent';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { PageViewRepository } from '../repositories/PageViewRepository';
|
||||
import { PageView } from '../../domain/entities/PageView';
|
||||
import type { EntityType, VisitorType } from '../../domain/types/PageView';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { PageView } from '../../domain/entities/PageView';
|
||||
import type { EntityType, VisitorType } from '../../domain/types/PageView';
|
||||
import type { PageViewRepository } from '../repositories/PageViewRepository';
|
||||
|
||||
export interface RecordPageViewInput {
|
||||
entityType: EntityType;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Entity } from '@core/shared/domain/Entity';
|
||||
import type { EntityType, PageViewProps, VisitorType } from '../types/PageView';
|
||||
import { AnalyticsEntityId } from '../value-objects/AnalyticsEntityId';
|
||||
import { AnalyticsSessionId } from '../value-objects/AnalyticsSessionId';
|
||||
import { PageViewId } from '../value-objects/PageViewId';
|
||||
|
||||
export type { EntityType, VisitorType } from '../types/PageView';
|
||||
|
||||
@@ -23,13 +22,11 @@ export class PageView extends Entity<string> {
|
||||
readonly timestamp: Date;
|
||||
readonly durationMs: number | undefined;
|
||||
|
||||
private readonly idVo: PageViewId;
|
||||
private readonly entityIdVo: AnalyticsEntityId;
|
||||
private readonly sessionIdVo: AnalyticsSessionId;
|
||||
|
||||
private constructor(props: PageViewProps) {
|
||||
super(props.id);
|
||||
this.idVo = PageViewId.create(props.id);
|
||||
this.entityType = props.entityType;
|
||||
this.entityIdVo = AnalyticsEntityId.create(props.entityId);
|
||||
this.visitorId = props.visitorId;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Defines persistence operations for AnalyticsSnapshot entities.
|
||||
*/
|
||||
|
||||
import type { AnalyticsSnapshot, SnapshotPeriod, SnapshotEntityType } from '../entities/AnalyticsSnapshot';
|
||||
import type { AnalyticsSnapshot, SnapshotEntityType, SnapshotPeriod } from '../entities/AnalyticsSnapshot';
|
||||
|
||||
export interface AnalyticsSnapshotRepository {
|
||||
save(snapshot: AnalyticsSnapshot): Promise<void>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EngagementEvent, EngagementAction, EngagementEntityType } from '../entities/EngagementEvent';
|
||||
import type { EngagementAction, EngagementEntityType, EngagementEvent } from '../entities/EngagementEvent';
|
||||
|
||||
export interface EngagementRepository {
|
||||
save(event: EngagementEvent): Promise<void>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { validateEmail, isDisposableEmail } from '@core/identity/domain/value-objects/EmailAddress';
|
||||
import { isDisposableEmail, validateEmail } from '@core/identity/domain/value-objects/EmailAddress';
|
||||
|
||||
describe('identity-domain email validation', () => {
|
||||
it('accepts a valid email and normalizes it', () => {
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
*/
|
||||
|
||||
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
||||
import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { GetLeagueEligibilityPreviewQuery, GetLeagueEligibilityPreviewQueryHandler } from './GetLeagueEligibilityPreviewQuery';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
|
||||
describe('GetLeagueEligibilityPreviewQuery', () => {
|
||||
let mockUserRatingRepo: UserRatingRepository;
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
* Uses EligibilityEvaluator to provide explainable results.
|
||||
*/
|
||||
|
||||
import { EvaluationResultDto, EligibilityFilterDto } from '../../domain/types/Eligibility';
|
||||
import { EligibilityEvaluator, RatingData } from '../../domain/services/EligibilityEvaluator';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { EligibilityEvaluator, RatingData } from '../../domain/services/EligibilityEvaluator';
|
||||
import { EligibilityFilterDto, EvaluationResultDto } from '../../domain/types/Eligibility';
|
||||
|
||||
export interface GetLeagueEligibilityPreviewQuery {
|
||||
userId: string;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Paginated/filtered query for user rating events (ledger).
|
||||
*/
|
||||
|
||||
import { PaginatedQueryOptions, RatingEventFilter, RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { LedgerEntryDto, LedgerFilter, PaginatedLedgerResult } from '../dtos/LedgerEntryDto';
|
||||
import { RatingEventRepository, PaginatedQueryOptions, RatingEventFilter } from '../../domain/repositories/RatingEventRepository';
|
||||
|
||||
export interface GetUserRatingLedgerQuery {
|
||||
userId: string;
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
* Tests for GetUserRatingsSummaryQuery
|
||||
*/
|
||||
|
||||
import { describe, expect, it, beforeEach, vi, type Mock } from 'vitest';
|
||||
import { GetUserRatingsSummaryQuery, GetUserRatingsSummaryQueryHandler } from './GetUserRatingsSummaryQuery';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
||||
import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
||||
import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { GetUserRatingsSummaryQuery, GetUserRatingsSummaryQueryHandler } from './GetUserRatingsSummaryQuery';
|
||||
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
* Combines platform snapshots and external game ratings.
|
||||
*/
|
||||
|
||||
import { RatingSummaryDto } from '../dtos/RatingSummaryDto';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { RatingSummaryDto } from '../dtos/RatingSummaryDto';
|
||||
|
||||
export interface GetUserRatingsSummaryQuery {
|
||||
userId: string;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { OpenAdminVoteSessionUseCase } from './OpenAdminVoteSessionUseCase';
|
||||
import { CastAdminVoteUseCase } from './CastAdminVoteUseCase';
|
||||
import { CloseAdminVoteSessionUseCase } from './CloseAdminVoteSessionUseCase';
|
||||
import { AdminVoteSession } from '../../domain/entities/AdminVoteSession';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
|
||||
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
||||
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { CastAdminVoteUseCase } from './CastAdminVoteUseCase';
|
||||
import { CloseAdminVoteSessionUseCase } from './CloseAdminVoteSessionUseCase';
|
||||
import { OpenAdminVoteSessionUseCase } from './OpenAdminVoteSessionUseCase';
|
||||
|
||||
// Mock Repository
|
||||
class MockAdminVoteSessionRepository {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AppendRatingEventsUseCase, AppendRatingEventsInput } from './AppendRatingEventsUseCase';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { AppendRatingEventsInput, AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
|
||||
describe('AppendRatingEventsUseCase', () => {
|
||||
let mockEventRepo: Partial<RatingEventRepository>;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { RatingEvent, type RatingEventProps } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
||||
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
||||
import { RatingEvent, type RatingEventProps } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { CreateRatingEventDto } from '../dtos/CreateRatingEventDto';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AdminVoteOutcome, AdminVoteSession } from '../../domain/entities/AdminVoteSession';
|
||||
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
||||
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
||||
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
||||
import { CloseAdminVoteSessionInput, CloseAdminVoteSessionOutput } from '../dtos/AdminVoteSessionDto';
|
||||
import { AdminVoteSession, AdminVoteOutcome } from '../../domain/entities/AdminVoteSession';
|
||||
|
||||
/**
|
||||
* Use Case: CloseAdminVoteSessionUseCase
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import { describe, expect, it, vi, type Mock, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { ForgotPasswordUseCase } from './ForgotPasswordUseCase';
|
||||
import type { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import type { MagicLinkRepository } from '../../domain/repositories/MagicLinkRepository';
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import type { MagicLinkNotificationPort } from '../ports/MagicLinkNotificationPort';
|
||||
import { ForgotPasswordUseCase } from './ForgotPasswordUseCase';
|
||||
|
||||
describe('ForgotPasswordUseCase', () => {
|
||||
let authRepo: {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import { MagicLinkRepository } from '../../domain/repositories/MagicLinkRepository';
|
||||
import { MagicLinkNotificationPort } from '../../domain/ports/MagicLinkNotificationPort';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { MagicLinkNotificationPort } from '../../domain/ports/MagicLinkNotificationPort';
|
||||
import { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import { MagicLinkRepository } from '../../domain/repositories/MagicLinkRepository';
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
|
||||
export type ForgotPasswordInput = {
|
||||
email: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { vi, type Mock, beforeEach, describe, it, expect } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import { StoredUser, type UserRepository } from '../../domain/repositories/UserRepository';
|
||||
import { GetCurrentSessionUseCase } from './GetCurrentSessionUseCase';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import { UserRepository } from '../../domain/repositories/UserRepository';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { describe, expect, it, vi, type Mock, beforeEach } from 'vitest';
|
||||
import { GetUserUseCase } from './GetUserUseCase';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import type { UserRepository } from '../../domain/repositories/UserRepository';
|
||||
import { GetUserUseCase } from './GetUserUseCase';
|
||||
|
||||
describe('GetUserUseCase', () => {
|
||||
let userRepo: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { User } from '../../domain/entities/User';
|
||||
import { UserRepository } from '../../domain/repositories/UserRepository';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import { UserRepository } from '../../domain/repositories/UserRepository';
|
||||
|
||||
export type GetUserInput = {
|
||||
userId: string;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { describe, expect, it, vi, type Mock, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import type { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { LoginUseCase } from './LoginUseCase';
|
||||
import type { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import type { PasswordHashingService } from '../ports/PasswordHashingService';
|
||||
import { LoginUseCase } from './LoginUseCase';
|
||||
|
||||
describe('LoginUseCase', () => {
|
||||
let authRepo: {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import { PasswordHashingService } from '../../domain/services/PasswordHashingService';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
|
||||
export type LoginInput = {
|
||||
email: string;
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
* Authenticates a user with email and password.
|
||||
*/
|
||||
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
import { UserRepository } from '../../domain/repositories/UserRepository';
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
|
||||
export type LoginWithEmailInput = {
|
||||
email: string;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
|
||||
export type LogoutInput = {};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
|
||||
import { AdminVoteSession } from '../../domain/entities/AdminVoteSession';
|
||||
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
|
||||
import { OpenAdminVoteSessionInput, OpenAdminVoteSessionOutput } from '../dtos/AdminVoteSessionDto';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { RecomputeUserRatingSnapshotUseCase } from './RecomputeUserRatingSnapshotUseCase';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { RecomputeUserRatingSnapshotUseCase } from './RecomputeUserRatingSnapshotUseCase';
|
||||
|
||||
describe('RecomputeUserRatingSnapshotUseCase', () => {
|
||||
let mockEventRepo: Partial<RatingEventRepository>;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { RecordRaceRatingEventsUseCase } from './RecordRaceRatingEventsUseCase';
|
||||
import { RaceResultsProvider, RaceResultsData } from '../ports/RaceResultsProvider';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { RaceResultsData, RaceResultsProvider } from '../ports/RaceResultsProvider';
|
||||
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
import { RecordRaceRatingEventsUseCase } from './RecordRaceRatingEventsUseCase';
|
||||
|
||||
// In-memory implementations for integration testing
|
||||
class InMemoryRaceResultsProvider implements RaceResultsProvider {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { RecordRaceRatingEventsUseCase } from './RecordRaceRatingEventsUseCase';
|
||||
import { RaceResultsProvider, RaceResultsData } from '../ports/RaceResultsProvider';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { RaceResultsData, RaceResultsProvider } from '../ports/RaceResultsProvider';
|
||||
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
import { RecordRaceRatingEventsUseCase } from './RecordRaceRatingEventsUseCase';
|
||||
|
||||
// Mock implementations
|
||||
class MockRaceResultsProvider implements RaceResultsProvider {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { RaceResultsProvider } from '../ports/RaceResultsProvider';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
||||
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
import { RecordRaceRatingEventsInput, RecordRaceRatingEventsOutput } from '../dtos/RecordRaceRatingEventsDto';
|
||||
import { RaceResultsProvider } from '../ports/RaceResultsProvider';
|
||||
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
|
||||
/**
|
||||
* Use Case: RecordRaceRatingEventsUseCase
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import { MagicLinkRepository } from '../../domain/repositories/MagicLinkRepository';
|
||||
import { PasswordHashingService } from '../../domain/services/PasswordHashingService';
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
|
||||
export type ResetPasswordInput = {
|
||||
token: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { describe, expect, it, vi, type Mock, beforeEach } from 'vitest';
|
||||
import { SignupSponsorUseCase } from './SignupSponsorUseCase';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import type { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import type { CompanyRepository } from '../../domain/repositories/CompanyRepository';
|
||||
import type { PasswordHashingService } from '../ports/PasswordHashingService';
|
||||
import { SignupSponsorUseCase } from './SignupSponsorUseCase';
|
||||
|
||||
describe('SignupSponsorUseCase', () => {
|
||||
let authRepo: {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Company } from '../../domain/entities/Company';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import { CompanyRepository } from '../../domain/repositories/CompanyRepository';
|
||||
import { PasswordHashingService } from '../../domain/services/PasswordHashingService';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
|
||||
export type SignupSponsorInput = {
|
||||
email: string;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { describe, expect, it, vi, type Mock, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import type { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { SignupUseCase } from './SignupUseCase';
|
||||
import type { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import type { PasswordHashingService } from '../ports/PasswordHashingService';
|
||||
import { SignupUseCase } from './SignupUseCase';
|
||||
|
||||
describe('SignupUseCase', () => {
|
||||
let authRepo: {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import { AuthRepository } from '../../domain/repositories/AuthRepository';
|
||||
import { PasswordHashingService } from '../../domain/services/PasswordHashingService';
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
|
||||
export type SignupInput = {
|
||||
email: string;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { describe, expect, it, vi, type Mock, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import type { UserRepository } from '../../domain/repositories/UserRepository';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
import { SignupWithEmailUseCase } from './SignupWithEmailUseCase';
|
||||
import type { UserRepository } from '../../domain/repositories/UserRepository';
|
||||
|
||||
describe('SignupWithEmailUseCase', () => {
|
||||
let userRepository: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { StoredUser, UserRepository } from '../../domain/repositories/UserRepository';
|
||||
import type { AuthenticatedUser } from '../ports/IdentityProviderPort';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase';
|
||||
import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto';
|
||||
import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase';
|
||||
|
||||
// Mock repository for integration test
|
||||
class MockExternalGameRatingRepository {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
||||
import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto';
|
||||
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
|
||||
import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase';
|
||||
|
||||
describe('UpsertExternalGameRatingUseCase', () => {
|
||||
let useCase: UpsertExternalGameRatingUseCase;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
||||
import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { UpsertExternalGameRatingInput, UpsertExternalGameRatingOutput } from '../dtos/UpsertExternalGameRatingDto';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IdentityDomainInvariantError, IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { AdminVoteSession } from './AdminVoteSession';
|
||||
import { IdentityDomainValidationError, IdentityDomainInvariantError } from '../errors/IdentityDomainError';
|
||||
|
||||
describe('AdminVoteSession', () => {
|
||||
const now = new Date('2025-01-01T00:00:00Z');
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ExternalGameRatingProfile } from './ExternalGameRatingProfile';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
import { GameKey } from '../value-objects/GameKey';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { ExternalRating } from '../value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../value-objects/ExternalRatingProvenance';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { GameKey } from '../value-objects/GameKey';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
import { ExternalGameRatingProfile } from './ExternalGameRatingProfile';
|
||||
|
||||
describe('ExternalGameRatingProfile', () => {
|
||||
let userId: UserId;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Entity } from '@core/shared/domain/Entity';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
import { GameKey } from '../value-objects/GameKey';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { ExternalRating } from '../value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../value-objects/ExternalRatingProvenance';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { GameKey } from '../value-objects/GameKey';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
|
||||
export interface ExternalGameRatingProfileProps {
|
||||
userId: UserId;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { RatingEvent } from './RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { IdentityDomainInvariantError, IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { IdentityDomainValidationError, IdentityDomainInvariantError } from '../errors/IdentityDomainError';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingEvent } from './RatingEvent';
|
||||
|
||||
describe('RatingEvent', () => {
|
||||
const validProps = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as mod from '@core/identity/domain/entities/SponsorAccount';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('identity/domain/entities/SponsorAccount.ts', () => {
|
||||
it('imports', () => {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* Separate from the racing domain's Sponsor entity which holds business data.
|
||||
*/
|
||||
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
import type { EmailValidationResult } from '../types/EmailAddress';
|
||||
import { validateEmail } from '../types/EmailAddress';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
|
||||
export interface SponsorAccountProps {
|
||||
id: UserId;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as mod from '@core/identity/domain/entities/User';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('identity/domain/entities/User.ts', () => {
|
||||
it('imports', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { StoredUser } from '../repositories/UserRepository';
|
||||
import type { EmailValidationResult } from '../types/EmailAddress';
|
||||
import { validateEmail } from '../types/EmailAddress';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
import { PasswordHash } from '../value-objects/PasswordHash';
|
||||
import { StoredUser } from '../repositories/UserRepository';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
|
||||
export interface UserProps {
|
||||
id: UserId;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as mod from '@core/identity/domain/entities/UserAchievement';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('identity/domain/entities/UserAchievement.ts', () => {
|
||||
it('imports', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DomainError, CommonDomainErrorKind } from '@core/shared/errors/DomainError';
|
||||
import type { CommonDomainErrorKind, DomainError } from '@core/shared/errors/DomainError';
|
||||
|
||||
export abstract class IdentityDomainError extends Error implements DomainError<CommonDomainErrorKind> {
|
||||
readonly type = 'domain' as const;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EmailAddress } from '../value-objects/EmailAddress';
|
||||
import { User } from '../entities/User';
|
||||
import { EmailAddress } from '../value-objects/EmailAddress';
|
||||
|
||||
/**
|
||||
* Domain Repository: AuthRepository
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { ExternalGameRatingRepository } from './ExternalGameRatingRepository';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { ExternalGameRatingProfile } from '../entities/ExternalGameRatingProfile';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
import { GameKey } from '../value-objects/GameKey';
|
||||
import { ExternalRating } from '../value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../value-objects/ExternalRatingProvenance';
|
||||
import { GameKey } from '../value-objects/GameKey';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
import { ExternalGameRatingRepository } from './ExternalGameRatingRepository';
|
||||
|
||||
/**
|
||||
* Test suite for ExternalGameRatingRepository interface
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
*/
|
||||
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { RatingEventRepository, FindByUserIdOptions, PaginatedQueryOptions, PaginatedResult } from './RatingEventRepository';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { FindByUserIdOptions, PaginatedQueryOptions, PaginatedResult, RatingEventRepository } from './RatingEventRepository';
|
||||
|
||||
// In-memory test implementation
|
||||
class InMemoryRatingEventRepository implements RatingEventRepository {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AdminTrustRatingCalculator, VoteOutcomeInput, SystemSignalInput } from './AdminTrustRatingCalculator';
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { AdminTrustRatingCalculator, SystemSignalInput, VoteOutcomeInput } from './AdminTrustRatingCalculator';
|
||||
|
||||
describe('AdminTrustRatingCalculator', () => {
|
||||
describe('calculate', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { AdminVoteOutcome } from '../entities/AdminVoteSession';
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DrivingRatingCalculator, DrivingRaceFactsDto } from './DrivingRatingCalculator';
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { DrivingRaceFactsDto, DrivingRatingCalculator } from './DrivingRatingCalculator';
|
||||
|
||||
describe('DrivingRatingCalculator', () => {
|
||||
describe('calculateFromRaceFacts', () => {
|
||||
|
||||
@@ -56,7 +56,6 @@ export class DrivingRatingCalculator {
|
||||
|
||||
// Incident penalty per incident
|
||||
private static readonly INCIDENT_PENALTY = -5;
|
||||
private static readonly MAJOR_INCIDENT_PENALTY = -15;
|
||||
|
||||
/**
|
||||
* Calculate driving rating deltas from race facts
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Tests for EligibilityEvaluator
|
||||
*/
|
||||
|
||||
import { EligibilityEvaluator, RatingData } from './EligibilityEvaluator';
|
||||
import { EligibilityFilterDto } from '../types/Eligibility';
|
||||
import { EligibilityEvaluator, RatingData } from './EligibilityEvaluator';
|
||||
|
||||
describe('EligibilityEvaluator', () => {
|
||||
let evaluator: EligibilityEvaluator;
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
* Provides explainable results with detailed reasons.
|
||||
*/
|
||||
|
||||
import {
|
||||
EvaluationResultDto,
|
||||
EvaluationReason,
|
||||
EligibilityFilterDto,
|
||||
ParsedEligibilityFilter,
|
||||
EligibilityCondition
|
||||
import {
|
||||
EligibilityCondition,
|
||||
EligibilityFilterDto,
|
||||
EvaluationReason,
|
||||
EvaluationResultDto,
|
||||
ParsedEligibilityFilter
|
||||
} from '../types/Eligibility';
|
||||
|
||||
export interface RatingData {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RatingEventFactory, RaceFactsDto } from './RatingEventFactory';
|
||||
import { RaceFactsDto, RatingEventFactory } from './RatingEventFactory';
|
||||
|
||||
describe('RatingEventFactory', () => {
|
||||
describe('createFromRaceFinish', () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { DrivingReasonCode } from '../value-objects/DrivingReasonCode';
|
||||
import { AdminTrustReasonCode } from '../value-objects/AdminTrustReasonCode';
|
||||
import { DrivingReasonCode } from '../value-objects/DrivingReasonCode';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
|
||||
// Existing interfaces
|
||||
interface RaceFinishInput {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { RatingSnapshotCalculator } from './RatingSnapshotCalculator';
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingSnapshotCalculator } from './RatingSnapshotCalculator';
|
||||
|
||||
describe('RatingSnapshotCalculator', () => {
|
||||
describe('calculate', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UserRating } from '../value-objects/UserRating';
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { UserRating } from '../value-objects/UserRating';
|
||||
|
||||
/**
|
||||
* Domain Service: RatingSnapshotCalculator
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
|
||||
import { RatingUpdateService } from './RatingUpdateService';
|
||||
import type { UserRatingRepository } from '../repositories/UserRatingRepository';
|
||||
import type { RatingEventRepository } from '../repositories/RatingEventRepository';
|
||||
import { UserRating } from '../value-objects/UserRating';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import type { RatingEventRepository } from '../repositories/RatingEventRepository';
|
||||
import type { UserRatingRepository } from '../repositories/UserRatingRepository';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { UserRating } from '../value-objects/UserRating';
|
||||
import { RatingUpdateService } from './RatingUpdateService';
|
||||
|
||||
describe('RatingUpdateService - Slice 7 Evolution', () => {
|
||||
let service: RatingUpdateService;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { DomainService } from '@core/shared/domain/Service';
|
||||
import type { UserRatingRepository } from '../repositories/UserRatingRepository';
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import type { RatingEventRepository } from '../repositories/RatingEventRepository';
|
||||
import type { UserRatingRepository } from '../repositories/UserRatingRepository';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingEventFactory } from './RatingEventFactory';
|
||||
import { RatingSnapshotCalculator } from './RatingSnapshotCalculator';
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
|
||||
/**
|
||||
* Domain Service: RatingUpdateService
|
||||
@@ -112,20 +112,6 @@ export class RatingUpdateService implements DomainService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update individual driver rating based on race result (LEGACY - DEPRECATED)
|
||||
* Kept for backward compatibility but now uses event-based approach
|
||||
*/
|
||||
private async updateDriverRating(result: {
|
||||
driverId: string;
|
||||
position: number;
|
||||
totalDrivers: number;
|
||||
incidents: number;
|
||||
startPosition: number;
|
||||
}): Promise<void> {
|
||||
// Delegate to new event-based approach
|
||||
await this.updateDriverRatingsAfterRace([result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update trust score based on sportsmanship actions (USES LEDGER)
|
||||
@@ -193,34 +179,5 @@ export class RatingUpdateService implements DomainService {
|
||||
await this.userRatingRepository.save(snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate performance score based on finishing position and field strength
|
||||
* (Utility method kept for reference, but now handled by RatingEventFactory)
|
||||
*/
|
||||
private calculatePerformanceScore(
|
||||
position: number,
|
||||
totalDrivers: number,
|
||||
startPosition: number
|
||||
): number {
|
||||
const positionScore = ((totalDrivers - position + 1) / totalDrivers) * 100;
|
||||
const positionsGained = startPosition - position;
|
||||
const gainBonus = Math.max(0, positionsGained * 2);
|
||||
const fieldStrengthMultiplier = 0.8 + (totalDrivers / 50);
|
||||
const rawScore = (positionScore + gainBonus) * fieldStrengthMultiplier;
|
||||
return Math.max(0, Math.min(100, rawScore));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate fairness score based on incident involvement
|
||||
* (Utility method kept for reference, but now handled by RatingEventFactory)
|
||||
*/
|
||||
private calculateFairnessScore(incidents: number, totalDrivers: number): number {
|
||||
let fairnessScore = 100;
|
||||
fairnessScore -= incidents * 15;
|
||||
const incidentRate = incidents / totalDrivers;
|
||||
if (incidentRate > 0.5) {
|
||||
fairnessScore -= 20;
|
||||
}
|
||||
return Math.max(0, Math.min(100, fairnessScore));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AdminTrustReasonCode, type AdminTrustReasonCodeValue } from './AdminTrustReasonCode';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AdminTrustReasonCode, type AdminTrustReasonCodeValue } from './AdminTrustReasonCode';
|
||||
|
||||
describe('AdminTrustReasonCode', () => {
|
||||
describe('create', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DrivingReasonCode, type DrivingReasonCodeValue } from './DrivingReasonCode';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DrivingReasonCode, type DrivingReasonCodeValue } from './DrivingReasonCode';
|
||||
|
||||
describe('DrivingReasonCode', () => {
|
||||
describe('create', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as mod from '@core/identity/domain/value-objects/EmailAddress';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('identity/domain/value-objects/EmailAddress.ts', () => {
|
||||
it('imports', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ValueObject } from '@core/shared/domain/ValueObject';
|
||||
import type { EmailValidationResult } from '../types/EmailAddress';
|
||||
import { validateEmail, isDisposableEmail } from '../types/EmailAddress';
|
||||
import { isDisposableEmail, validateEmail } from '../types/EmailAddress';
|
||||
|
||||
export interface EmailAddressProps {
|
||||
value: string;
|
||||
@@ -44,5 +44,6 @@ export class EmailAddress implements ValueObject<EmailAddressProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export { isDisposableEmail, validateEmail } from '../types/EmailAddress';
|
||||
export type { EmailValidationResult } from '../types/EmailAddress';
|
||||
export { validateEmail, isDisposableEmail } from '../types/EmailAddress';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { ExternalRating } from './ExternalRating';
|
||||
import { GameKey } from './GameKey';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
|
||||
describe('ExternalRating', () => {
|
||||
describe('create', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExternalRatingProvenance } from './ExternalRatingProvenance';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { ExternalRatingProvenance } from './ExternalRatingProvenance';
|
||||
|
||||
describe('ExternalRatingProvenance', () => {
|
||||
describe('create', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GameKey } from './GameKey';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { GameKey } from './GameKey';
|
||||
|
||||
describe('GameKey', () => {
|
||||
describe('create', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as mod from '@core/identity/domain/value-objects/PasswordHash';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('identity/domain/value-objects/PasswordHash.ts', () => {
|
||||
it('imports', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import type { ValueObject } from '@core/shared/domain/ValueObject';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
export interface PasswordHashProps {
|
||||
value: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RatingDelta } from './RatingDelta';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { RatingDelta } from './RatingDelta';
|
||||
|
||||
describe('RatingDelta', () => {
|
||||
describe('create', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RatingDimensionKey } from './RatingDimensionKey';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { RatingDimensionKey } from './RatingDimensionKey';
|
||||
|
||||
describe('RatingDimensionKey', () => {
|
||||
describe('create', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RatingEventId } from './RatingEventId';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { RatingEventId } from './RatingEventId';
|
||||
|
||||
describe('RatingEventId', () => {
|
||||
describe('create', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ValueObject } from '@core/shared/domain/ValueObject';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
|
||||
export interface RatingEventIdProps {
|
||||
value: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RatingReference } from './RatingReference';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { RatingReference } from './RatingReference';
|
||||
|
||||
describe('RatingReference', () => {
|
||||
describe('create', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RatingValue } from './RatingValue';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { RatingValue } from './RatingValue';
|
||||
|
||||
describe('RatingValue', () => {
|
||||
describe('create', () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user