mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1886: feat: add unit tests for remission return receipt functionality
feat: add unit tests for remission return receipt functionality - Add tests for 4 new RemissionReturnReceiptService methods: - removeReturnItemFromReturnReceipt() - completeReturnReceipt() - completeReturn() - completeReturnReceiptAndReturn() - Update RemissionReturnReceiptDetailsCardComponent tests for itemCount -> positionCount - Add tests for new inputs and remove functionality in RemissionReturnReceiptDetailsItemComponent - Add tests for canRemoveItems and completeReturn in RemissionReturnReceiptDetailsComponent - All tests focus on happy path scenarios and isolated functionality Refs: #5138
This commit is contained in:
committed by
Nino Righi
parent
65ab3bfc0a
commit
b015e97e1f
@@ -1,166 +1,166 @@
|
||||
import { LogLevel } from './log-level.enum';
|
||||
import { Type } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Represents a destination where log messages are sent.
|
||||
* Implement this interface to create custom logging destinations like
|
||||
* console logging, remote logging services, or file logging.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* export class CustomLogSink implements Sink {
|
||||
* log(
|
||||
* level: LogLevel,
|
||||
* message: string,
|
||||
* context?: LoggerContext,
|
||||
* error?: Error
|
||||
* ): void {
|
||||
* // Custom logging implementation
|
||||
* if (level === LogLevel.Error) {
|
||||
* // Send to monitoring service
|
||||
* this.monitoringService.reportError(message, error, context);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface Sink {
|
||||
/**
|
||||
* Method called by the LoggingService to send a log entry to this sink.
|
||||
*
|
||||
* @param level - The severity level of the log message
|
||||
* @param message - The main log message content
|
||||
* @param context - Optional structured data or metadata about the log event
|
||||
* @param error - Optional error object when logging errors
|
||||
*/
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: LoggerContext,
|
||||
error?: Error,
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function that creates a logging sink function.
|
||||
* Useful when the sink needs access to injected dependencies or
|
||||
* requires initialization logic.
|
||||
*
|
||||
* @returns A function matching the Sink.log method signature
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const httpLogSink: SinkFn = () => {
|
||||
* const http = inject(HttpClient);
|
||||
* const config = inject(ConfigService);
|
||||
*
|
||||
* return (level, message, context?, error?) => {
|
||||
* http.post(config.loggingEndpoint, {
|
||||
* level,
|
||||
* message,
|
||||
* context,
|
||||
* error: error && {
|
||||
* name: error.name,
|
||||
* message: error.message,
|
||||
* stack: error.stack
|
||||
* }
|
||||
* }).subscribe();
|
||||
* };
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export type SinkFn = () => (
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: LoggerContext,
|
||||
error?: Error,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Configuration options for the logging service.
|
||||
* Used to set up the logging behavior during application initialization.
|
||||
*/
|
||||
export interface LoggingConfig {
|
||||
/** The minimum log level to process. Messages below this level are ignored. */
|
||||
level: LogLevel;
|
||||
|
||||
/**
|
||||
* An array of logging destinations where messages will be sent.
|
||||
* Can be sink instances, classes, or factory functions.
|
||||
*/
|
||||
sinks: (Sink | SinkFn | Type<Sink>)[];
|
||||
|
||||
/**
|
||||
* Optional global context included with every log message.
|
||||
* Useful for adding application-wide metadata like version or environment.
|
||||
*/
|
||||
context?: LoggerContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the public API for logging operations.
|
||||
* This interface is returned by the logger factory and provides
|
||||
* methods for logging at different severity levels.
|
||||
*/
|
||||
export interface LoggerApi {
|
||||
/**
|
||||
* Logs a trace message with optional context.
|
||||
* Use for fine-grained debugging information.
|
||||
*/
|
||||
trace(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs a debug message with optional context.
|
||||
* Use for development-time debugging information.
|
||||
*/
|
||||
debug(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs an info message with optional context.
|
||||
* Use for general runtime information.
|
||||
*/
|
||||
info(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs a warning message with optional context.
|
||||
* Use for potentially harmful situations.
|
||||
*/
|
||||
warn(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs an error message with an optional error object and context.
|
||||
* Use for error conditions that affect functionality.
|
||||
*
|
||||
* @param message - The error message to log
|
||||
* @param error - Optional error object that caused this error condition
|
||||
* @param context - Optional context data associated with the error
|
||||
*/
|
||||
error(message: string, error?: Error, context?: () => LoggerContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents context data associated with a log message.
|
||||
* Context allows adding structured metadata to log messages,
|
||||
* making them more informative and easier to filter/analyze.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Component context
|
||||
* const context: LoggerContext = {
|
||||
* component: 'UserProfile',
|
||||
* userId: '12345',
|
||||
* action: 'save'
|
||||
* };
|
||||
*
|
||||
* // Error context
|
||||
* const errorContext: LoggerContext = {
|
||||
* operationId: 'op-123',
|
||||
* attemptNumber: 3,
|
||||
* inputData: { ... }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export interface LoggerContext {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
import { LogLevel } from './log-level.enum';
|
||||
import { Type } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Represents a destination where log messages are sent.
|
||||
* Implement this interface to create custom logging destinations like
|
||||
* console logging, remote logging services, or file logging.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* export class CustomLogSink implements Sink {
|
||||
* log(
|
||||
* level: LogLevel,
|
||||
* message: string,
|
||||
* context?: LoggerContext,
|
||||
* error?: Error
|
||||
* ): void {
|
||||
* // Custom logging implementation
|
||||
* if (level === LogLevel.Error) {
|
||||
* // Send to monitoring service
|
||||
* this.monitoringService.reportError(message, error, context);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface Sink {
|
||||
/**
|
||||
* Method called by the LoggingService to send a log entry to this sink.
|
||||
*
|
||||
* @param level - The severity level of the log message
|
||||
* @param message - The main log message content
|
||||
* @param context - Optional structured data or metadata about the log event
|
||||
* @param error - Optional error object when logging errors
|
||||
*/
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: LoggerContext,
|
||||
error?: Error,
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function that creates a logging sink function.
|
||||
* Useful when the sink needs access to injected dependencies or
|
||||
* requires initialization logic.
|
||||
*
|
||||
* @returns A function matching the Sink.log method signature
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const httpLogSink: SinkFn = () => {
|
||||
* const http = inject(HttpClient);
|
||||
* const config = inject(ConfigService);
|
||||
*
|
||||
* return (level, message, context?, error?) => {
|
||||
* http.post(config.loggingEndpoint, {
|
||||
* level,
|
||||
* message,
|
||||
* context,
|
||||
* error: error && {
|
||||
* name: error.name,
|
||||
* message: error.message,
|
||||
* stack: error.stack
|
||||
* }
|
||||
* }).subscribe();
|
||||
* };
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export type SinkFn = () => (
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: LoggerContext,
|
||||
error?: Error,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Configuration options for the logging service.
|
||||
* Used to set up the logging behavior during application initialization.
|
||||
*/
|
||||
export interface LoggingConfig {
|
||||
/** The minimum log level to process. Messages below this level are ignored. */
|
||||
level: LogLevel;
|
||||
|
||||
/**
|
||||
* An array of logging destinations where messages will be sent.
|
||||
* Can be sink instances, classes, or factory functions.
|
||||
*/
|
||||
sinks: (Sink | SinkFn | Type<Sink>)[];
|
||||
|
||||
/**
|
||||
* Optional global context included with every log message.
|
||||
* Useful for adding application-wide metadata like version or environment.
|
||||
*/
|
||||
context?: LoggerContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the public API for logging operations.
|
||||
* This interface is returned by the logger factory and provides
|
||||
* methods for logging at different severity levels.
|
||||
*/
|
||||
export interface LoggerApi {
|
||||
/**
|
||||
* Logs a trace message with optional context.
|
||||
* Use for fine-grained debugging information.
|
||||
*/
|
||||
trace(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs a debug message with optional context.
|
||||
* Use for development-time debugging information.
|
||||
*/
|
||||
debug(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs an info message with optional context.
|
||||
* Use for general runtime information.
|
||||
*/
|
||||
info(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs a warning message with optional context.
|
||||
* Use for potentially harmful situations.
|
||||
*/
|
||||
warn(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs an error message with an optional error object and context.
|
||||
* Use for error conditions that affect functionality.
|
||||
*
|
||||
* @param message - The error message to log
|
||||
* @param error - Optional error object that caused this error condition
|
||||
* @param context - Optional context data associated with the error
|
||||
*/
|
||||
error(message: string, error?: unknown, context?: () => LoggerContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents context data associated with a log message.
|
||||
* Context allows adding structured metadata to log messages,
|
||||
* making them more informative and easier to filter/analyze.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Component context
|
||||
* const context: LoggerContext = {
|
||||
* component: 'UserProfile',
|
||||
* userId: '12345',
|
||||
* action: 'save'
|
||||
* };
|
||||
*
|
||||
* // Error context
|
||||
* const errorContext: LoggerContext = {
|
||||
* operationId: 'op-123',
|
||||
* attemptNumber: 3,
|
||||
* inputData: { ... }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export interface LoggerContext {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ describe('RemissionReturnReceiptService', () => {
|
||||
let mockReturnService: {
|
||||
ReturnQueryReturns: jest.Mock;
|
||||
ReturnGetReturnReceipt: jest.Mock;
|
||||
ReturnRemoveReturnItem: jest.Mock;
|
||||
ReturnFinalizeReceipt: jest.Mock;
|
||||
ReturnFinalizeReturn: jest.Mock;
|
||||
};
|
||||
let mockRemissionStockService: {
|
||||
fetchAssignedStock: jest.Mock;
|
||||
@@ -66,6 +69,9 @@ describe('RemissionReturnReceiptService', () => {
|
||||
mockReturnService = {
|
||||
ReturnQueryReturns: jest.fn(),
|
||||
ReturnGetReturnReceipt: jest.fn(),
|
||||
ReturnRemoveReturnItem: jest.fn(),
|
||||
ReturnFinalizeReceipt: jest.fn(),
|
||||
ReturnFinalizeReturn: jest.fn(),
|
||||
};
|
||||
|
||||
mockRemissionStockService = {
|
||||
@@ -412,4 +418,194 @@ describe('RemissionReturnReceiptService', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeReturnItemFromReturnReceipt', () => {
|
||||
it('should remove item from return receipt successfully', async () => {
|
||||
mockReturnService.ReturnRemoveReturnItem.mockReturnValue(
|
||||
of({ result: null, error: null }),
|
||||
);
|
||||
|
||||
const params = {
|
||||
returnId: 1,
|
||||
receiptId: 101,
|
||||
receiptItemId: 1001,
|
||||
};
|
||||
|
||||
await service.removeReturnItemFromReturnReceipt(params);
|
||||
|
||||
expect(mockReturnService.ReturnRemoveReturnItem).toHaveBeenCalledWith(
|
||||
params,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns error', async () => {
|
||||
const errorResponse = { error: 'API Error', result: null };
|
||||
mockReturnService.ReturnRemoveReturnItem.mockReturnValue(
|
||||
of(errorResponse),
|
||||
);
|
||||
|
||||
const params = {
|
||||
returnId: 1,
|
||||
receiptId: 101,
|
||||
receiptItemId: 1001,
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.removeReturnItemFromReturnReceipt(params),
|
||||
).rejects.toThrow(ResponseArgsError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeReturnReceipt', () => {
|
||||
const mockCompletedReceipt: Receipt = {
|
||||
id: 101,
|
||||
receiptNumber: 'REC-2024-001',
|
||||
completed: '2024-01-15T10:30:00.000Z',
|
||||
created: '2024-01-15T09:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt;
|
||||
|
||||
it('should complete return receipt successfully', async () => {
|
||||
mockReturnService.ReturnFinalizeReceipt.mockReturnValue(
|
||||
of({ result: mockCompletedReceipt, error: null }),
|
||||
);
|
||||
|
||||
const params = {
|
||||
returnId: 1,
|
||||
receiptId: 101,
|
||||
};
|
||||
|
||||
const result = await service.completeReturnReceipt(params);
|
||||
|
||||
expect(result).toEqual(mockCompletedReceipt);
|
||||
expect(mockReturnService.ReturnFinalizeReceipt).toHaveBeenCalledWith({
|
||||
returnId: 1,
|
||||
receiptId: 101,
|
||||
data: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns error', async () => {
|
||||
const errorResponse = { error: 'API Error', result: null };
|
||||
mockReturnService.ReturnFinalizeReceipt.mockReturnValue(
|
||||
of(errorResponse),
|
||||
);
|
||||
|
||||
const params = {
|
||||
returnId: 1,
|
||||
receiptId: 101,
|
||||
};
|
||||
|
||||
await expect(service.completeReturnReceipt(params)).rejects.toThrow(
|
||||
ResponseArgsError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeReturn', () => {
|
||||
const mockCompletedReturn: Return = {
|
||||
id: 1,
|
||||
receipts: [
|
||||
{
|
||||
id: 101,
|
||||
data: {
|
||||
id: 101,
|
||||
receiptNumber: 'REC-2024-001',
|
||||
completed: '2024-01-15T10:30:00.000Z',
|
||||
created: '2024-01-15T09:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as unknown as Return;
|
||||
|
||||
it('should complete return successfully', async () => {
|
||||
mockReturnService.ReturnFinalizeReturn.mockReturnValue(
|
||||
of({ result: mockCompletedReturn, error: null }),
|
||||
);
|
||||
|
||||
const params = { returnId: 1 };
|
||||
|
||||
const result = await service.completeReturn(params);
|
||||
|
||||
expect(result).toEqual(mockCompletedReturn);
|
||||
expect(mockReturnService.ReturnFinalizeReturn).toHaveBeenCalledWith(
|
||||
params,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns error', async () => {
|
||||
const errorResponse = { error: 'API Error', result: null };
|
||||
mockReturnService.ReturnFinalizeReturn.mockReturnValue(of(errorResponse));
|
||||
|
||||
const params = { returnId: 1 };
|
||||
|
||||
await expect(service.completeReturn(params)).rejects.toThrow(
|
||||
ResponseArgsError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeReturnReceiptAndReturn', () => {
|
||||
const mockCompletedReceipt: Receipt = {
|
||||
id: 101,
|
||||
receiptNumber: 'REC-2024-001',
|
||||
completed: '2024-01-15T10:30:00.000Z',
|
||||
created: '2024-01-15T09:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt;
|
||||
|
||||
const mockCompletedReturn: Return = {
|
||||
id: 1,
|
||||
receipts: [
|
||||
{
|
||||
id: 101,
|
||||
data: mockCompletedReceipt,
|
||||
},
|
||||
],
|
||||
} as unknown as Return;
|
||||
|
||||
it('should complete both receipt and return successfully', async () => {
|
||||
mockReturnService.ReturnFinalizeReceipt.mockReturnValue(
|
||||
of({ result: mockCompletedReceipt, error: null }),
|
||||
);
|
||||
mockReturnService.ReturnFinalizeReturn.mockReturnValue(
|
||||
of({ result: mockCompletedReturn, error: null }),
|
||||
);
|
||||
|
||||
const params = {
|
||||
returnId: 1,
|
||||
receiptId: 101,
|
||||
};
|
||||
|
||||
const result = await service.completeReturnReceiptAndReturn(params);
|
||||
|
||||
expect(result).toEqual(mockCompletedReturn);
|
||||
expect(mockReturnService.ReturnFinalizeReceipt).toHaveBeenCalledWith({
|
||||
returnId: 1,
|
||||
receiptId: 101,
|
||||
data: {},
|
||||
});
|
||||
expect(mockReturnService.ReturnFinalizeReturn).toHaveBeenCalledWith({
|
||||
returnId: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error if completeReturnReceipt fails', async () => {
|
||||
const errorResponse = { error: 'API Error', result: null };
|
||||
mockReturnService.ReturnFinalizeReceipt.mockReturnValue(
|
||||
of(errorResponse),
|
||||
);
|
||||
|
||||
const params = {
|
||||
returnId: 1,
|
||||
receiptId: 101,
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.completeReturnReceiptAndReturn(params),
|
||||
).rejects.toThrow(ResponseArgsError);
|
||||
expect(mockReturnService.ReturnFinalizeReturn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,14 +15,14 @@ import { logger } from '@isa/core/logging';
|
||||
/**
|
||||
* Service responsible for managing remission return receipts.
|
||||
* Handles fetching completed and incomplete return receipts from the inventory API.
|
||||
*
|
||||
*
|
||||
* @class RemissionReturnReceiptService
|
||||
* @injectable
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // Inject the service
|
||||
* constructor(private remissionReturnReceiptService: RemissionReturnReceiptService) {}
|
||||
*
|
||||
*
|
||||
* // Fetch completed receipts
|
||||
* const completedReceipts = await this.remissionReturnReceiptService
|
||||
* .fetchCompletedRemissionReturnReceipts();
|
||||
@@ -39,12 +39,12 @@ export class RemissionReturnReceiptService {
|
||||
/**
|
||||
* Fetches all completed remission return receipts for the assigned stock.
|
||||
* Returns receipts marked as completed within the last 7 days.
|
||||
*
|
||||
*
|
||||
* @async
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Return[]>} Array of completed return objects with receipts
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const controller = new AbortController();
|
||||
* const completedReturns = await service
|
||||
@@ -54,15 +54,15 @@ export class RemissionReturnReceiptService {
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Return[]> {
|
||||
this.#logger.debug('Fetching completed remission return receipts');
|
||||
|
||||
|
||||
const assignedStock =
|
||||
await this.#remissionStockService.fetchAssignedStock(abortSignal);
|
||||
|
||||
this.#logger.info('Fetching completed returns from API', () => ({
|
||||
stockId: assignedStock.id,
|
||||
startDate: subDays(new Date(), 7).toISOString()
|
||||
startDate: subDays(new Date(), 7).toISOString(),
|
||||
}));
|
||||
|
||||
|
||||
let req$ = this.#returnService.ReturnQueryReturns({
|
||||
stockId: assignedStock.id,
|
||||
queryToken: {
|
||||
@@ -80,27 +80,30 @@ export class RemissionReturnReceiptService {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error('Failed to fetch completed returns', new Error(res.message || 'Unknown error'));
|
||||
this.#logger.error(
|
||||
'Failed to fetch completed returns',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const returns = (res?.result as Return[]) || [];
|
||||
this.#logger.debug('Successfully fetched completed returns', () => ({
|
||||
returnCount: returns.length
|
||||
returnCount: returns.length,
|
||||
}));
|
||||
|
||||
|
||||
return returns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all incomplete remission return receipts for the assigned stock.
|
||||
* Returns receipts not yet marked as completed within the last 7 days.
|
||||
*
|
||||
*
|
||||
* @async
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Return[]>} Array of incomplete return objects with receipts
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const incompleteReturns = await service
|
||||
* .fetchIncompletedRemissionReturnReceipts();
|
||||
@@ -109,15 +112,15 @@ export class RemissionReturnReceiptService {
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Return[]> {
|
||||
this.#logger.debug('Fetching incomplete remission return receipts');
|
||||
|
||||
|
||||
const assignedStock =
|
||||
await this.#remissionStockService.fetchAssignedStock(abortSignal);
|
||||
|
||||
this.#logger.info('Fetching incomplete returns from API', () => ({
|
||||
stockId: assignedStock.id,
|
||||
startDate: subDays(new Date(), 7).toISOString()
|
||||
startDate: subDays(new Date(), 7).toISOString(),
|
||||
}));
|
||||
|
||||
|
||||
let req$ = this.#returnService.ReturnQueryReturns({
|
||||
stockId: assignedStock.id,
|
||||
queryToken: {
|
||||
@@ -135,22 +138,25 @@ export class RemissionReturnReceiptService {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error('Failed to fetch incomplete returns', new Error(res.message || 'Unknown error'));
|
||||
this.#logger.error(
|
||||
'Failed to fetch incomplete returns',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const returns = (res?.result as Return[]) || [];
|
||||
this.#logger.debug('Successfully fetched incomplete returns', () => ({
|
||||
returnCount: returns.length
|
||||
returnCount: returns.length,
|
||||
}));
|
||||
|
||||
|
||||
return returns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a specific remission return receipt by receipt and return IDs.
|
||||
* Validates parameters using FetchRemissionReturnReceiptSchema before making the request.
|
||||
*
|
||||
*
|
||||
* @async
|
||||
* @param {FetchRemissionReturnParams} params - The receipt and return identifiers
|
||||
* @param {FetchRemissionReturnParams} params.receiptId - ID of the receipt to fetch
|
||||
@@ -159,7 +165,7 @@ export class RemissionReturnReceiptService {
|
||||
* @returns {Promise<Receipt | undefined>} The receipt object if found, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const receipt = await service.fetchRemissionReturnReceipt({
|
||||
* receiptId: '123',
|
||||
@@ -171,15 +177,15 @@ export class RemissionReturnReceiptService {
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Receipt | undefined> {
|
||||
this.#logger.debug('Fetching remission return receipt', () => ({ params }));
|
||||
|
||||
|
||||
const { receiptId, returnId } =
|
||||
FetchRemissionReturnReceiptSchema.parse(params);
|
||||
|
||||
this.#logger.info('Fetching return receipt from API', () => ({
|
||||
receiptId,
|
||||
returnId
|
||||
returnId,
|
||||
}));
|
||||
|
||||
|
||||
let req$ = this.#returnService.ReturnGetReturnReceipt({
|
||||
receiptId,
|
||||
returnId,
|
||||
@@ -194,15 +200,113 @@ export class RemissionReturnReceiptService {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error('Failed to fetch return receipt', new Error(res.message || 'Unknown error'));
|
||||
this.#logger.error(
|
||||
'Failed to fetch return receipt',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const receipt = res?.result as Receipt | undefined;
|
||||
this.#logger.debug('Successfully fetched return receipt', () => ({
|
||||
found: !!receipt
|
||||
found: !!receipt,
|
||||
}));
|
||||
|
||||
|
||||
return receipt;
|
||||
}
|
||||
|
||||
async removeReturnItemFromReturnReceipt(params: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
receiptItemId: number;
|
||||
}) {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnRemoveReturnItem(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to remove item from return receipt',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
}
|
||||
|
||||
async completeReturnReceipt({
|
||||
returnId,
|
||||
receiptId,
|
||||
}: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
}): Promise<Receipt> {
|
||||
this.#logger.debug('Completing return receipt', () => ({ returnId }));
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnFinalizeReceipt({
|
||||
returnId,
|
||||
receiptId,
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to complete return receipt',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
return res?.result as Receipt;
|
||||
}
|
||||
|
||||
async completeReturn(params: { returnId: number }): Promise<Return> {
|
||||
this.#logger.debug('Completing return', () => ({
|
||||
returnId: params.returnId,
|
||||
}));
|
||||
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnFinalizeReturn(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to complete return',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
this.#logger.info('Successfully completed return', () => ({
|
||||
returnId: params.returnId,
|
||||
}));
|
||||
|
||||
return res?.result as Return;
|
||||
}
|
||||
|
||||
async completeReturnReceiptAndReturn(params: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
}): Promise<Return> {
|
||||
this.#logger.debug('Completing return receipt and return', () => ({
|
||||
returnId: params.returnId,
|
||||
receiptId: params.receiptId,
|
||||
}));
|
||||
|
||||
await this.completeReturnReceipt({
|
||||
returnId: params.returnId,
|
||||
receiptId: params.receiptId,
|
||||
});
|
||||
|
||||
const completedReturn = await this.completeReturn({
|
||||
returnId: params.returnId,
|
||||
});
|
||||
|
||||
this.#logger.info('Successfully completed return and receipt', () => ({
|
||||
returnId: params.returnId,
|
||||
receiptId: params.receiptId,
|
||||
}));
|
||||
|
||||
return completedReturn;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
class="isa-text-body-1-bold"
|
||||
*uiSkeletonLoader="loading(); height: '1.5rem'"
|
||||
>
|
||||
{{ itemCount() }}
|
||||
{{ positionCount() }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
:host {
|
||||
@apply p-6 bg-isa-neutral-400 rounded-2xl grid grid-cols-3 gap-6 isa-text-body-1-regular;
|
||||
@apply p-6 bg-isa-neutral-400 rounded-2xl grid grid-cols-3 desktop-large:grid-cols-5 gap-6 isa-text-body-1-regular justify-evenly;
|
||||
}
|
||||
|
||||
@@ -127,28 +127,28 @@ describe('RemissionReturnReceiptDetailsCardComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemCount computed signal', () => {
|
||||
it('should calculate total quantity from items', () => {
|
||||
describe('positionCount computed signal', () => {
|
||||
it('should return the number of items', () => {
|
||||
fixture.componentRef.setInput('receipt', mockReceipt);
|
||||
|
||||
// mockReceipt has items with quantities 5 and 3 = 8 total
|
||||
expect(component.itemCount()).toBe(8);
|
||||
// mockReceipt has 2 items
|
||||
expect(component.positionCount()).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 0 when no items', () => {
|
||||
const receiptWithoutItems = { ...mockReceipt, items: [] };
|
||||
fixture.componentRef.setInput('receipt', receiptWithoutItems);
|
||||
|
||||
expect(component.itemCount()).toBe(0);
|
||||
expect(component.positionCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should return undefined when no receipt provided', () => {
|
||||
fixture.componentRef.setInput('receipt', undefined);
|
||||
|
||||
expect(component.itemCount()).toBeUndefined();
|
||||
expect(component.positionCount()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle items with undefined data', () => {
|
||||
it('should count all items regardless of data', () => {
|
||||
const receiptWithUndefinedItems = {
|
||||
...mockReceipt,
|
||||
items: [
|
||||
@@ -158,7 +158,7 @@ describe('RemissionReturnReceiptDetailsCardComponent', () => {
|
||||
};
|
||||
fixture.componentRef.setInput('receipt', receiptWithUndefinedItems);
|
||||
|
||||
expect(component.itemCount()).toBe(5);
|
||||
expect(component.positionCount()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,7 +282,7 @@ describe('RemissionReturnReceiptDetailsCardComponent', () => {
|
||||
(component.supplierResource as any).value = signal(mockSuppliers);
|
||||
|
||||
expect(component.status()).toBe('Abgeschlossen');
|
||||
expect(component.itemCount()).toBe(8);
|
||||
expect(component.positionCount()).toBe(2);
|
||||
|
||||
// Change receipt
|
||||
const newReceipt = {
|
||||
@@ -293,7 +293,7 @@ describe('RemissionReturnReceiptDetailsCardComponent', () => {
|
||||
fixture.componentRef.setInput('receipt', newReceipt);
|
||||
|
||||
expect(component.status()).toBe('Offen');
|
||||
expect(component.itemCount()).toBe(10);
|
||||
expect(component.positionCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('should create supplier resource on initialization', () => {
|
||||
|
||||
@@ -12,11 +12,11 @@ import { createSupplierResource } from './resources';
|
||||
/**
|
||||
* Component that displays detailed information about a remission return receipt in a card format.
|
||||
* Shows supplier information, status, dates, item counts, and package numbers.
|
||||
*
|
||||
*
|
||||
* @component
|
||||
* @selector remi-remission-return-receipt-details-card
|
||||
* @standalone
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* <remi-remission-return-receipt-details-card
|
||||
* [receipt]="receiptData"
|
||||
@@ -59,15 +59,12 @@ export class RemissionReturnReceiptDetailsCardComponent {
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that calculates the total quantity of all items in the receipt.
|
||||
* @returns {number} Sum of all item quantities
|
||||
* Computed signal that calculates the items in the receipt.
|
||||
* @returns {number} Count of items in the receipt or 0 if not available
|
||||
*/
|
||||
itemCount = computed(() => {
|
||||
positionCount = computed(() => {
|
||||
const receipt = this.receipt();
|
||||
return receipt?.items?.reduce(
|
||||
(acc, item) => acc + (item.data?.quantity || 0),
|
||||
0,
|
||||
);
|
||||
return receipt?.items?.length;
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,9 +24,12 @@
|
||||
<div class="flex justify-center items-center">
|
||||
@if (removeable()) {
|
||||
<ui-icon-button
|
||||
[pending]="removing()"
|
||||
[name]="'isaActionClose'"
|
||||
[size]="'large'"
|
||||
[color]="'secondary'"
|
||||
(click)="remove()"
|
||||
[disabled]="removing()"
|
||||
></ui-icon-button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
|
||||
import {
|
||||
ReceiptItem,
|
||||
RemissionProductGroupService,
|
||||
RemissionReturnReceiptService,
|
||||
} from '@isa/remission/data-access';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
@@ -45,6 +46,10 @@ describe('RemissionReturnReceiptDetailsItemComponent', () => {
|
||||
fetchProductGroups: vi.fn().mockResolvedValue(mockProductGroups),
|
||||
};
|
||||
|
||||
const mockRemissionReturnReceiptService = {
|
||||
removeReturnItemFromReturnReceipt: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RemissionReturnReceiptDetailsItemComponent],
|
||||
@@ -53,6 +58,10 @@ describe('RemissionReturnReceiptDetailsItemComponent', () => {
|
||||
provide: RemissionProductGroupService,
|
||||
useValue: mockRemissionProductGroupService,
|
||||
},
|
||||
{
|
||||
provide: RemissionReturnReceiptService,
|
||||
useValue: mockRemissionReturnReceiptService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(RemissionReturnReceiptDetailsItemComponent, {
|
||||
@@ -387,4 +396,98 @@ describe('RemissionReturnReceiptDetailsItemComponent', () => {
|
||||
// This would improve E2E test reliability and maintainability
|
||||
});
|
||||
});
|
||||
|
||||
describe('New inputs - receiptId and returnId', () => {
|
||||
it('should accept receiptId input', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('receiptId', 123);
|
||||
|
||||
expect(component.receiptId()).toBe(123);
|
||||
});
|
||||
|
||||
it('should accept returnId input', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('returnId', 456);
|
||||
|
||||
expect(component.returnId()).toBe(456);
|
||||
});
|
||||
|
||||
it('should handle both receiptId and returnId inputs together', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('receiptId', 123);
|
||||
fixture.componentRef.setInput('returnId', 456);
|
||||
|
||||
expect(component.receiptId()).toBe(123);
|
||||
expect(component.returnId()).toBe(456);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remove functionality', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('receiptId', 123);
|
||||
fixture.componentRef.setInput('returnId', 456);
|
||||
});
|
||||
|
||||
it('should initialize removing signal as false', () => {
|
||||
expect(component.removing()).toBe(false);
|
||||
});
|
||||
|
||||
it('should call service and emit removed event on successful remove', async () => {
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
|
||||
let emittedItem: ReceiptItem | undefined;
|
||||
component.removed.subscribe((item) => {
|
||||
emittedItem = item;
|
||||
});
|
||||
|
||||
await component.remove();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt,
|
||||
).toHaveBeenCalledWith({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
receiptItemId: 1,
|
||||
});
|
||||
expect(emittedItem).toEqual(mockReceiptItem);
|
||||
expect(component.removing()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle remove error gracefully', async () => {
|
||||
const mockError = new Error('Remove failed');
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt.mockRejectedValue(
|
||||
mockError,
|
||||
);
|
||||
|
||||
let emittedItem: ReceiptItem | undefined;
|
||||
component.removed.subscribe((item) => {
|
||||
emittedItem = item;
|
||||
});
|
||||
|
||||
await component.remove();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt,
|
||||
).toHaveBeenCalledWith({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
receiptItemId: 1,
|
||||
});
|
||||
expect(emittedItem).toBeUndefined();
|
||||
expect(component.removing()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not call service if already removing', async () => {
|
||||
component.removing.set(true);
|
||||
|
||||
await component.remove();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,16 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { ReceiptItem } from '@isa/remission/data-access';
|
||||
import {
|
||||
ReceiptItem,
|
||||
RemissionReturnReceiptService,
|
||||
ReturnItem,
|
||||
} from '@isa/remission/data-access';
|
||||
import { ProductFormatComponent } from '@isa/shared/product-foramt';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
|
||||
@@ -13,6 +20,7 @@ import { productGroupResource } from './resources';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionClose } from '@isa/icons';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
/**
|
||||
* Component for displaying a single receipt item within the remission return receipt details.
|
||||
@@ -43,6 +51,11 @@ import { isaActionClose } from '@isa/icons';
|
||||
providers: [provideIcons({ isaActionClose })],
|
||||
})
|
||||
export class RemissionReturnReceiptDetailsItemComponent {
|
||||
#logger = logger(() => ({
|
||||
component: 'RemissionReturnReceiptDetailsItemComponent',
|
||||
}));
|
||||
#returnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
/**
|
||||
* Required input for the receipt item to display.
|
||||
* Contains product information and quantity details.
|
||||
@@ -51,6 +64,10 @@ export class RemissionReturnReceiptDetailsItemComponent {
|
||||
*/
|
||||
item = input.required<ReceiptItem>();
|
||||
|
||||
receiptId = input.required<number>();
|
||||
|
||||
returnId = input.required<number>();
|
||||
|
||||
removeable = input<boolean>(false);
|
||||
|
||||
productGroupResource = productGroupResource();
|
||||
@@ -66,4 +83,26 @@ export class RemissionReturnReceiptDetailsItemComponent {
|
||||
?.value || ''
|
||||
);
|
||||
});
|
||||
|
||||
removing = signal(false);
|
||||
|
||||
removed = output<ReceiptItem>();
|
||||
|
||||
async remove() {
|
||||
if (this.removing()) {
|
||||
return;
|
||||
}
|
||||
this.removing.set(true);
|
||||
try {
|
||||
await this.#returnReceiptService.removeReturnItemFromReturnReceipt({
|
||||
receiptId: this.receiptId(),
|
||||
returnId: this.returnId(),
|
||||
receiptItemId: this.item().id,
|
||||
});
|
||||
this.removed.emit(this.item());
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to remove item', error);
|
||||
}
|
||||
this.removing.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
<remi-remission-return-receipt-details-item
|
||||
[item]="item.data"
|
||||
[removeable]="canRemoveItems()"
|
||||
[receiptId]="receiptId()"
|
||||
[returnId]="returnId()"
|
||||
(removed)="returnResource.reload()"
|
||||
></remi-remission-return-receipt-details-item>
|
||||
@if (!last) {
|
||||
<hr class="border-isa-neutral-300" />
|
||||
@@ -50,3 +53,22 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!returnResource.isLoading() && !returnResource.value()?.completed) {
|
||||
<ui-stateful-button
|
||||
class="fixed right-6 bottom-6"
|
||||
(click)="completeReturn()"
|
||||
[(state)]="completeReturnState"
|
||||
defaultContent="Wanne abschließen"
|
||||
defaultWidth="13rem"
|
||||
[errorContent]="completeReturnError()"
|
||||
errorWidth="32rem"
|
||||
errorAction="Erneut versuchen"
|
||||
(action)="completeReturn()"
|
||||
successContent="Wanne abgeschlossen"
|
||||
successWidth="20rem"
|
||||
[pending]="completingReturn()"
|
||||
size="large"
|
||||
color="brand"
|
||||
>
|
||||
</ui-stateful-button>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Location } from '@angular/common';
|
||||
import { RemissionReturnReceiptDetailsComponent } from './remission-return-receipt-details.component';
|
||||
import { RemissionReturnReceiptDetailsCardComponent } from './remission-return-receipt-details-card.component';
|
||||
import { RemissionReturnReceiptDetailsItemComponent } from './remission-return-receipt-details-item.component';
|
||||
import { Receipt } from '@isa/remission/data-access';
|
||||
import { Receipt, RemissionReturnReceiptService } from '@isa/remission/data-access';
|
||||
|
||||
// Mock the resource function
|
||||
vi.mock('./resources/remission-return-receipt.resource', () => ({
|
||||
@@ -29,11 +29,19 @@ describe('RemissionReturnReceiptDetailsComponent', () => {
|
||||
created: new Date('2024-01-15T10:30:00Z'),
|
||||
} as Receipt;
|
||||
|
||||
const mockRemissionReturnReceiptService = {
|
||||
completeReturnReceiptAndReturn: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RemissionReturnReceiptDetailsComponent],
|
||||
providers: [
|
||||
MockProvider(Location, { back: vi.fn() }),
|
||||
{
|
||||
provide: RemissionReturnReceiptService,
|
||||
useValue: mockRemissionReturnReceiptService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(RemissionReturnReceiptDetailsComponent, {
|
||||
@@ -157,4 +165,115 @@ describe('RemissionReturnReceiptDetailsComponent', () => {
|
||||
expect(component.returnResource.isLoading()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canRemoveItems computed signal', () => {
|
||||
it('should return true when receipt is not completed', () => {
|
||||
const incompleteReceipt = {
|
||||
...mockReceipt,
|
||||
completed: false,
|
||||
};
|
||||
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
(component.returnResource as any).value = signal(incompleteReceipt);
|
||||
|
||||
expect(component.canRemoveItems()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when receipt is completed', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
(component.returnResource as any).value = signal(mockReceipt);
|
||||
|
||||
expect(component.canRemoveItems()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no receipt data', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
(component.returnResource as any).value = signal(null);
|
||||
|
||||
expect(component.canRemoveItems()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeReturn functionality', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
(component.returnResource as any).reload = vi.fn();
|
||||
});
|
||||
|
||||
it('should initialize completion state signals', () => {
|
||||
expect(component.completeReturnState()).toBe('default');
|
||||
expect(component.completingReturn()).toBe(false);
|
||||
expect(component.completeReturnError()).toBe(null);
|
||||
});
|
||||
|
||||
it('should complete return successfully', async () => {
|
||||
const mockCompletedReturn = { ...mockReceipt, completed: true };
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
mockCompletedReturn,
|
||||
);
|
||||
|
||||
await component.completeReturn();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn,
|
||||
).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(component.completeReturnState()).toBe('success');
|
||||
expect(component.completingReturn()).toBe(false);
|
||||
expect(component.completeReturnError()).toBe(null);
|
||||
expect(component.returnResource.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle completion error', async () => {
|
||||
const mockError = new Error('Completion failed');
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockRejectedValue(
|
||||
mockError,
|
||||
);
|
||||
|
||||
await component.completeReturn();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn,
|
||||
).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(component.completeReturnState()).toBe('error');
|
||||
expect(component.completingReturn()).toBe(false);
|
||||
expect(component.completeReturnError()).toBe('Completion failed');
|
||||
expect(component.returnResource.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle non-Error objects', async () => {
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockRejectedValue(
|
||||
'String error',
|
||||
);
|
||||
|
||||
await component.completeReturn();
|
||||
|
||||
expect(component.completeReturnState()).toBe('error');
|
||||
expect(component.completeReturnError()).toBe(
|
||||
'Wanne konnte nicht abgeschlossen werden',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not process if already completing', async () => {
|
||||
component.completingReturn.set(true);
|
||||
|
||||
await component.completeReturn();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,15 +4,23 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { coerceNumberProperty, NumberInput } from '@angular/cdk/coercion';
|
||||
import { ButtonComponent, IconButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
ButtonComponent,
|
||||
IconButtonComponent,
|
||||
StatefulButtonComponent,
|
||||
StatefulButtonState,
|
||||
} from '@isa/ui/buttons';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionChevronLeft, isaLoading } from '@isa/icons';
|
||||
import { RemissionReturnReceiptDetailsCardComponent } from './remission-return-receipt-details-card.component';
|
||||
import { RemissionReturnReceiptDetailsItemComponent } from './remission-return-receipt-details-item.component';
|
||||
import { Location } from '@angular/common';
|
||||
import { createRemissionReturnReceiptResource } from './resources/remission-return-receipt.resource';
|
||||
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
/**
|
||||
* Component for displaying detailed information about a remission return receipt.
|
||||
@@ -40,10 +48,17 @@ import { createRemissionReturnReceiptResource } from './resources/remission-retu
|
||||
NgIcon,
|
||||
RemissionReturnReceiptDetailsCardComponent,
|
||||
RemissionReturnReceiptDetailsItemComponent,
|
||||
StatefulButtonComponent,
|
||||
],
|
||||
providers: [provideIcons({ isaActionChevronLeft, isaLoading })],
|
||||
})
|
||||
export class RemissionReturnReceiptDetailsComponent {
|
||||
#logger = logger(() => ({
|
||||
component: 'RemissionReturnReceiptDetailsComponent',
|
||||
}));
|
||||
|
||||
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
/** Angular Location service for navigation */
|
||||
location = inject(Location);
|
||||
|
||||
@@ -94,4 +109,32 @@ export class RemissionReturnReceiptDetailsComponent {
|
||||
const ret = this.returnResource.value();
|
||||
return !ret?.completed;
|
||||
});
|
||||
|
||||
completeReturnState = signal<StatefulButtonState>('default');
|
||||
completingReturn = signal(false);
|
||||
completeReturnError = signal<string | null>(null);
|
||||
|
||||
async completeReturn() {
|
||||
if (this.completingReturn()) {
|
||||
return;
|
||||
}
|
||||
this.completingReturn.set(true);
|
||||
try {
|
||||
await this.#remissionReturnReceiptService.completeReturnReceiptAndReturn({
|
||||
returnId: this.returnId(),
|
||||
receiptId: this.receiptId(),
|
||||
});
|
||||
this.completeReturnState.set('success');
|
||||
this.returnResource.reload();
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to complete return', error);
|
||||
this.completeReturnError.set(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Wanne konnte nicht abgeschlossen werden',
|
||||
);
|
||||
this.completeReturnState.set('error');
|
||||
}
|
||||
this.completingReturn.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export class StatefulButtonComponent implements OnDestroy {
|
||||
errorWidth = input<string>('100%');
|
||||
|
||||
// Optional dismiss timeout in milliseconds
|
||||
dismiss = input<number>();
|
||||
dismiss = input<number>(5000);
|
||||
|
||||
// Button properties
|
||||
color = input<ButtonColor>('primary');
|
||||
|
||||
56666
package-lock.json
generated
56666
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user