Merged PR 1977: #5390 Reward Checkout Action Card - Collect Request

#5390 Reward Checkout Action Card - Collect Request
This commit is contained in:
Nino Righi
2025-10-22 13:08:52 +00:00
committed by Lorenz Hilpert
parent bcb412e48d
commit 7376846894
36 changed files with 1470 additions and 132 deletions

View File

@@ -37,6 +37,7 @@ export { Gender } from './models/gender';
export { DateRangeDTO } from './models/date-range-dto';
export { PaymentType } from './models/payment-type';
export { PaymentStatus } from './models/payment-status';
export { LoyaltyDTO } from './models/loyalty-dto';
export { QueryTokenDTO } from './models/query-token-dto';
export { ListResponseArgsOfOrderItemListItemDTO } from './models/list-response-args-of-order-item-list-item-dto';
export { ResponseArgsOfIEnumerableOfOrderItemListItemDTO } from './models/response-args-of-ienumerable-of-order-item-list-item-dto';
@@ -130,7 +131,6 @@ export { Price } from './models/price';
export { ShippingTarget } from './models/shipping-target';
export { EntityDTOBaseOfShopItemDTOAndIShopItem } from './models/entity-dtobase-of-shop-item-dtoand-ishop-item';
export { CampaignDTO } from './models/campaign-dto';
export { LoyaltyDTO } from './models/loyalty-dto';
export { EntityDTOBaseOfOrderItemDTOAndIOrderItem } from './models/entity-dtobase-of-order-item-dtoand-iorder-item';
export { EntityDTOContainerOfSupplierDTO } from './models/entity-dtocontainer-of-supplier-dto';
export { SupplierDTO } from './models/supplier-dto';
@@ -207,6 +207,8 @@ export { EntityDTOContainerOfOrderItemSubsetTransitionDTO } from './models/entit
export { OrderItemSubsetTransitionDTO } from './models/order-item-subset-transition-dto';
export { EntityDTOBaseOfOrderItemSubsetTransitionDTOAndIOrderItemStatusTransition } from './models/entity-dtobase-of-order-item-subset-transition-dtoand-iorder-item-status-transition';
export { EntityDTOBaseOfOrderItemSubsetTaskDTOAndIOrderItemStatusTask } from './models/entity-dtobase-of-order-item-subset-task-dtoand-iorder-item-status-task';
export { LoyaltyCollectValues } from './models/loyalty-collect-values';
export { LoyaltyCollectType } from './models/loyalty-collect-type';
export { ResponseArgsOfBoolean } from './models/response-args-of-boolean';
export { ResponseArgsOfOrderItemDTO } from './models/response-args-of-order-item-dto';
export { ResponseArgsOfIEnumerableOfOrderItemDTO } from './models/response-args-of-ienumerable-of-order-item-dto';

View File

@@ -1,5 +1,6 @@
/* tslint:disable */
import { EntityDTOBaseOfDisplayOrderItemDTOAndIOrderItem } from './entity-dtobase-of-display-order-item-dtoand-iorder-item';
import { LoyaltyDTO } from './loyalty-dto';
import { DisplayOrderDTO } from './display-order-dto';
import { PriceDTO } from './price-dto';
import { ProductDTO } from './product-dto';
@@ -23,6 +24,11 @@ export interface DisplayOrderItemDTO extends EntityDTOBaseOfDisplayOrderItemDTOA
*/
features?: {[key: string]: string};
/**
* Loylty
*/
loyalty?: LoyaltyDTO;
/**
* Bestellung
*/

View File

@@ -0,0 +1,2 @@
/* tslint:disable */
export type LoyaltyCollectType = 0 | 1 | 2;

View File

@@ -0,0 +1,18 @@
/* tslint:disable */
import { LoyaltyCollectType } from './loyalty-collect-type';
/**
* Loyalty collect values
*/
export interface LoyaltyCollectValues {
/**
* Collect Type
*/
collectType: LoyaltyCollectType;
/**
* Quantity (optional, default null)
*/
quantity?: number;
}

View File

@@ -4,6 +4,7 @@ import { EnvironmentChannel } from './environment-channel';
import { CRUDA } from './cruda';
import { DateRangeDTO } from './date-range-dto';
import { Gender } from './gender';
import { LoyaltyDTO } from './loyalty-dto';
import { OrderType } from './order-type';
import { PaymentStatus } from './payment-status';
import { PaymentType } from './payment-type';
@@ -102,6 +103,11 @@ export interface OrderItemListItemDTO {
*/
lastName?: string;
/**
* Loylty
*/
loyalty?: LoyaltyDTO;
/**
* Bestellfiliale
*/

View File

@@ -1,12 +1,19 @@
/* tslint:disable */
import { EntityReferenceDTO } from './entity-reference-dto';
import { KeyValueDTOOfStringAndString } from './key-value-dtoof-string-and-string';
import { CRUDA } from './cruda';
import { PriceDTO } from './price-dto';
import { LoyaltyDTO } from './loyalty-dto';
import { ProductDTO } from './product-dto';
import { PromotionDTO } from './promotion-dto';
import { QuantityDTO } from './quantity-dto';
import { ReceiptListItemDTO } from './receipt-list-item-dto';
export interface ReceiptItemListItemDTO extends EntityReferenceDTO{
/**
* Mögliche Aktionen
*/
actions?: Array<KeyValueDTOOfStringAndString>;
buyerComment?: string;
/**
@@ -19,6 +26,11 @@ export interface ReceiptItemListItemDTO extends EntityReferenceDTO{
*/
discountedPrice?: PriceDTO;
/**
* Zusätzliche Markierungen
*/
features?: {[key: string]: string};
/**
* PK
*/
@@ -35,6 +47,11 @@ export interface ReceiptItemListItemDTO extends EntityReferenceDTO{
*/
lineNumber?: number;
/**
* Loyalty
*/
loyalty?: LoyaltyDTO;
/**
* Bestellnummer
*/

View File

@@ -21,6 +21,8 @@ import { ResponseArgsOfQuerySettingsDTO } from '../models/response-args-of-query
import { ResponseArgsOfIEnumerableOfAutocompleteDTO } from '../models/response-args-of-ienumerable-of-autocomplete-dto';
import { AutocompleteTokenDTO } from '../models/autocomplete-token-dto';
import { ListResponseArgsOfDBHOrderItemListItemDTO } from '../models/list-response-args-of-dbhorder-item-list-item-dto';
import { ResponseArgsOfIEnumerableOfDBHOrderItemListItemDTO } from '../models/response-args-of-ienumerable-of-dbhorder-item-list-item-dto';
import { LoyaltyCollectValues } from '../models/loyalty-collect-values';
import { ListResponseArgsOfOrderItemListItemDTO } from '../models/list-response-args-of-order-item-list-item-dto';
import { ResponseArgsOfIEnumerableOfOrderItemDTO } from '../models/response-args-of-ienumerable-of-order-item-dto';
import { OrderItemDTO } from '../models/order-item-dto';
@@ -54,6 +56,7 @@ class OrderService extends __BaseService {
static readonly OrderKundenbestellungenSettingsPath = '/kundenbestellungen/s/settings';
static readonly OrderKundenbestellungenAutocompletePath = '/kundenbestellungen/s/complete';
static readonly OrderKundenbestellungenPath = '/kundenbestellungen/s';
static readonly OrderLoyaltyCollectPath = '/order/{orderId}/orderitem/{orderItemId}/orderitemsubset/{orderItemSubsetId}/loyaltycollect';
static readonly OrderQueryOrderItemPath = '/order/item/s';
static readonly OrderQueryOrderItemAutocompletePath = '/order/item/s/complete';
static readonly OrderGetOrderItemPath = '/order/orderitem/{orderItemId}';
@@ -636,6 +639,63 @@ class OrderService extends __BaseService {
);
}
/**
* Ausgabe order Storno von Prämienbestellposten
* Falls die Menge/Stückzahl kleiner der ursprünglichen Menge/Stückzahl ist, wird eine neue Bestellpostenteilmenge erzeugt.
* @param params The `OrderService.OrderLoyaltyCollectParams` containing the following parameters:
*
* - `orderItemSubsetId`: PK Bestellpostenteilmenge
*
* - `orderItemId`: PK Bestellposten
*
* - `orderId`: PK Bestellung
*
* - `data`: Daten zur Änderung des Bearbeitungsstatus
*/
OrderLoyaltyCollectResponse(params: OrderService.OrderLoyaltyCollectParams): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfDBHOrderItemListItemDTO>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
__body = params.data;
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/order/${encodeURIComponent(String(params.orderId))}/orderitem/${encodeURIComponent(String(params.orderItemId))}/orderitemsubset/${encodeURIComponent(String(params.orderItemSubsetId))}/loyaltycollect`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfIEnumerableOfDBHOrderItemListItemDTO>;
})
);
}
/**
* Ausgabe order Storno von Prämienbestellposten
* Falls die Menge/Stückzahl kleiner der ursprünglichen Menge/Stückzahl ist, wird eine neue Bestellpostenteilmenge erzeugt.
* @param params The `OrderService.OrderLoyaltyCollectParams` containing the following parameters:
*
* - `orderItemSubsetId`: PK Bestellpostenteilmenge
*
* - `orderItemId`: PK Bestellposten
*
* - `orderId`: PK Bestellung
*
* - `data`: Daten zur Änderung des Bearbeitungsstatus
*/
OrderLoyaltyCollect(params: OrderService.OrderLoyaltyCollectParams): __Observable<ResponseArgsOfIEnumerableOfDBHOrderItemListItemDTO> {
return this.OrderLoyaltyCollectResponse(params).pipe(
__map(_r => _r.body as ResponseArgsOfIEnumerableOfDBHOrderItemListItemDTO)
);
}
/**
* Suche nach Bestellposten
* @param queryToken Suchkriterien
@@ -1671,6 +1731,32 @@ module OrderService {
buyerNumber?: null | string;
}
/**
* Parameters for OrderLoyaltyCollect
*/
export interface OrderLoyaltyCollectParams {
/**
* PK Bestellpostenteilmenge
*/
orderItemSubsetId: number;
/**
* PK Bestellposten
*/
orderItemId: number;
/**
* PK Bestellung
*/
orderId: number;
/**
* Daten zur Änderung des Bearbeitungsstatus
*/
data: LoyaltyCollectValues;
}
/**
* Parameters for OrderUpdateOrderItem
*/

View File

@@ -236,6 +236,7 @@ class ReceiptService extends __BaseService {
}
/**
* Aufgabe auf erledigt setzen
* @param taskId undefined
*/
ReceiptReceiptItemTaskCompletedResponse(taskId: number): __Observable<__StrictHttpResponse<ResponseArgsOfReceiptItemTaskListItemDTO>> {
@@ -261,6 +262,7 @@ class ReceiptService extends __BaseService {
);
}
/**
* Aufgabe auf erledigt setzen
* @param taskId undefined
*/
ReceiptReceiptItemTaskCompleted(taskId: number): __Observable<ResponseArgsOfReceiptItemTaskListItemDTO> {

View File

@@ -0,0 +1,62 @@
<div
class="w-[24.5rem] h-full p-4 flex flex-col gap-4 rounded-lg bg-isa-secondary-100"
[class.confirmation-list-item-done]="item().status !== 1"
>
@if (!isComplete()) {
<div
data-what="confirmation-message"
data-which="confirmation-comment"
class="isa-text-body-2-bold"
>
Bitte buchen Sie die Prämie aus dem Abholfach aus oder wählen Sie eine
andere Aktion.
</div>
<div class="flex flex-row justify-between items-center">
<ui-dropdown
class="h-8 border-none pl-0 hover:bg-transparent"
[value]="selectedAction()"
(valueChange)="setDropdownAction($event)"
>
<ui-dropdown-option [value]="LoyaltyCollectType.Collect"
>Prämie ausbuchen</ui-dropdown-option
>
<ui-dropdown-option [value]="LoyaltyCollectType.OutOfStock"
>Nicht gefunden</ui-dropdown-option
>
<ui-dropdown-option [value]="LoyaltyCollectType.Cancel"
>Stornieren</ui-dropdown-option
>
</ui-dropdown>
<button
class="flex items-center gap-2 self-end"
type="button"
uiButton
color="primary"
size="small"
(click)="onCollect()"
data-what="button"
data-which="complete"
>
<ng-icon name="isaActionCheck" uiButtonIcon></ng-icon>
Abschließen
</button>
</div>
} @else {
<div
data-what="done-message"
data-which="done-comment"
class="isa-text-body-2-bold"
>
Artikel wurde Storniert und Lesepunkte gut geschrieben.
</div>
<span
class="flex items-center gap-2 self-end text-isa-accent-green isa-text-body-2-bold"
>
<ng-icon name="isaActionCheck"></ng-icon>
Abgeschlossen
</span>
}
</div>

View File

@@ -1,13 +1,111 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { DisplayOrderItem } from '@isa/oms/data-access';
import {
ChangeDetectionStrategy,
Component,
input,
inject,
computed,
signal,
effect,
} from '@angular/core';
import { isaActionCheck } from '@isa/icons';
import {
DisplayOrderItem,
OrderRewardCollectFacade,
LoyaltyCollectType,
OrderItemSubsetResource,
getProcessingStatusCompleted,
} from '@isa/oms/data-access';
import { ButtonComponent } from '@isa/ui/buttons';
import { NgIcon } from '@ng-icons/core';
import { provideIcons } from '@ng-icons/core';
import { OrderConfiramtionStore } from '../../../reward-order-confirmation.store';
import {
DropdownButtonComponent,
DropdownOptionComponent,
} from '@isa/ui/input-controls';
@Component({
selector: 'checkout-confirmation-list-item-action-card',
templateUrl: './confirmation-list-item-action-card.component.html',
styleUrls: ['./confirmation-list-item-action-card.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [],
imports: [
NgIcon,
ButtonComponent,
DropdownButtonComponent,
DropdownOptionComponent,
],
providers: [provideIcons({ isaActionCheck }), OrderItemSubsetResource],
})
export class ConfirmationListItemActionCardComponent {
LoyaltyCollectType = LoyaltyCollectType;
#orderRewardCollectFacade = inject(OrderRewardCollectFacade);
#store = inject(OrderConfiramtionStore);
#orderItemSubsetResource = inject(OrderItemSubsetResource);
item = input.required<DisplayOrderItem>();
orders = this.#store.orders;
getOrderIdBasedOnItem = computed(() => {
const item = this.item();
const orders = this.orders();
if (!orders) {
return undefined;
}
const order = orders.find((order) =>
order.items?.some((orderItem) => orderItem.id === item.id),
);
return order?.id;
});
orderItemSubsets = this.#orderItemSubsetResource.orderItemSubsets;
selectedAction = signal<LoyaltyCollectType>(LoyaltyCollectType.Collect);
isComplete = computed(() => {
const subsets = this.orderItemSubsets();
const statuses = subsets?.map((subset) => subset.processingStatus);
return getProcessingStatusCompleted(statuses);
});
constructor() {
effect(() => {
const item = this.item();
const orderItemSubsetIds = item.subsetItems
?.map((subset) => subset.id)
?.filter((id): id is number => id !== undefined);
if (orderItemSubsetIds?.length) {
this.#orderItemSubsetResource.loadOrderItemSubsets(orderItemSubsetIds);
}
});
}
setDropdownAction(value: LoyaltyCollectType) {
this.selectedAction.set(value);
}
async onCollect() {
const item = this.item();
const orderId = this.getOrderIdBasedOnItem();
const orderItemId = item.id;
const collectType = this.selectedAction();
if (orderId && orderItemId) {
for (const subsetItem of item.subsetItems ?? []) {
const orderItemSubsetId = subsetItem.id;
const quantity = subsetItem.quantity;
if (orderItemSubsetId && !!quantity) {
await this.#orderRewardCollectFacade.collect({
orderId,
orderItemId,
orderItemSubsetId,
collectType,
quantity,
});
}
}
this.#orderItemSubsetResource.refresh();
}
}
}

View File

@@ -1,3 +1,3 @@
:host {
@apply flex w-full items-start gap-6;
}
:host {
@apply flex w-full items-start gap-6;
}

View File

@@ -0,0 +1,8 @@
import z from 'zod';
export const DateRangeSchema = z.object({
start: z.string().optional(),
end: z.string().optional(),
});
export type DateRange = z.infer<typeof DateRangeSchema>;

View File

@@ -2,6 +2,7 @@ export * from './address.schema';
export * from './addressee-with-reference.schema';
export * from './buyer-type.schema';
export * from './communication-details.schema';
export * from './date-range.schema';
export * from './entity-container.schema';
export * from './entity-reference-container.schema';
export * from './entity-reference.schema';

View File

@@ -19,8 +19,9 @@ export * from './lib/errors';
export * from './lib/questions';
export * from './lib/models';
export * from './lib/facades';
export * from './lib/helpers/return-process';
export * from './lib/helpers';
export * from './lib/schemas';
export * from './lib/services';
export * from './lib/operators';
export * from './lib/stores';
export * from './lib/resources';

View File

@@ -1 +1,2 @@
export { OrderCreationFacade } from './order-creation.facade';
export { OrderRewardCollectFacade } from './order-reward-collect.facade';

View File

@@ -0,0 +1,105 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { OrderRewardCollectFacade } from './order-reward-collect.facade';
import { OrderRewardCollectService } from '../services';
import { DBHOrderItemListItemDTO } from '@generated/swagger/oms-api';
describe('OrderRewardCollectFacade', () => {
let spectator: SpectatorService<OrderRewardCollectFacade>;
const createService = createServiceFactory({
service: OrderRewardCollectFacade,
mocks: [OrderRewardCollectService],
});
beforeEach(() => {
spectator = createService();
});
describe('collect', () => {
it('should delegate to OrderRewardCollectService.collect', async () => {
// Arrange
const mockParams = {
orderId: 123,
orderItemId: 456,
orderItemSubsetId: 789,
collectType: 1 as const, // Valid LoyaltyCollectType.Collect
quantity: 2,
};
const mockResult: DBHOrderItemListItemDTO[] = [
{ orderItemType: 1 } as DBHOrderItemListItemDTO,
];
const rewardItemService = spectator.inject(OrderRewardCollectService);
(rewardItemService.collect as jest.Mock).mockResolvedValue(mockResult);
// Act
const result = await spectator.service.collect(mockParams);
// Assert
expect(result).toEqual(mockResult);
expect(rewardItemService.collect).toHaveBeenCalledWith({
orderId: mockParams.orderId,
orderItemId: mockParams.orderItemId,
orderItemSubsetId: mockParams.orderItemSubsetId,
collectType: mockParams.collectType,
quantity: mockParams.quantity,
});
expect(rewardItemService.collect).toHaveBeenCalledTimes(1);
});
it('should propagate errors from OrderRewardCollectService', async () => {
// Arrange
const mockParams = {
orderId: 123,
orderItemId: 456,
orderItemSubsetId: 789,
collectType: 1 as const, // Valid LoyaltyCollectType.Collect
quantity: 2,
};
const errorMessage = 'Failed to collect reward item';
const rewardItemService = spectator.inject(OrderRewardCollectService);
(rewardItemService.collect as jest.Mock).mockRejectedValue(
new Error(errorMessage),
);
// Act & Assert
await expect(spectator.service.collect(mockParams)).rejects.toThrow(
errorMessage,
);
expect(rewardItemService.collect).toHaveBeenCalledWith({
orderId: mockParams.orderId,
orderItemId: mockParams.orderItemId,
orderItemSubsetId: mockParams.orderItemSubsetId,
collectType: mockParams.collectType,
quantity: mockParams.quantity,
});
});
});
describe('getOrderItemSubset', () => {
it('should delegate to OrderRewardCollectService.fetchOrderItemSubset', async () => {
// Arrange
const mockParams = {
orderItemSubsetId: 789,
};
const mockResult = {
id: 789,
quantity: 2,
name: 'Test Subset',
};
const rewardItemService = spectator.inject(OrderRewardCollectService);
(rewardItemService.fetchOrderItemSubset as jest.Mock).mockResolvedValue(
mockResult,
);
// Act
const result = await spectator.service.getOrderItemSubset(mockParams);
// Assert
expect(result).toEqual(mockResult);
expect(rewardItemService.fetchOrderItemSubset).toHaveBeenCalledWith(
mockParams,
undefined,
);
expect(rewardItemService.fetchOrderItemSubset).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,25 @@
import { inject, Injectable } from '@angular/core';
import { OrderRewardCollectService } from '../services';
import {
FetchOrderItemSubsetSchemaInput,
OrderLoyaltyCollectInput,
} from '../schemas';
@Injectable({ providedIn: 'root' })
export class OrderRewardCollectFacade {
#orderRewardCollectService = inject(OrderRewardCollectService);
async collect(params: OrderLoyaltyCollectInput) {
return this.#orderRewardCollectService.collect(params);
}
async getOrderItemSubset(
params: FetchOrderItemSubsetSchemaInput,
abortSignal?: AbortSignal,
) {
return this.#orderRewardCollectService.fetchOrderItemSubset(
params,
abortSignal,
);
}
}

View File

@@ -0,0 +1,2 @@
export * from './return-process';
export * from './reward';

View File

@@ -0,0 +1,97 @@
import { OrderItemProcessingStatusValue } from '../../schemas';
import { getProcessingStatusCompleted } from './get-processing-status-completed.helper';
describe('getProcessingStatusCompleted', () => {
it('should return true when all statuses are different from Bestellt (16)', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Versendet, // 64
OrderItemProcessingStatusValue.Eingetroffen, // 128
OrderItemProcessingStatusValue.Abgeholt, // 256
];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(true);
});
it('should return true when statuses include various completed states', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Zugestellt, // 4194304
OrderItemProcessingStatusValue.Abgeholt, // 256
OrderItemProcessingStatusValue.Versendet, // 64
];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(true);
});
it('should return false when at least one status is Bestellt (16)', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Versendet, // 64
OrderItemProcessingStatusValue.Bestellt, // 16
OrderItemProcessingStatusValue.Abgeholt, // 256
];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(false);
});
it('should return false when all statuses are Bestellt (16)', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Bestellt, // 16
OrderItemProcessingStatusValue.Bestellt, // 16
OrderItemProcessingStatusValue.Bestellt, // 16
];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(false);
});
it('should return false when array is empty', () => {
// Arrange
const statuses: number[] = [];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(false);
});
it('should return false when statuses is undefined', () => {
// Arrange
const statuses = undefined;
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(false);
});
it('should return true with single status different from Bestellt', () => {
// Arrange
const statuses = [OrderItemProcessingStatusValue.Abgeholt]; // 256
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(true);
});
});

View File

@@ -0,0 +1,18 @@
import { OrderItemProcessingStatusValue } from '../../schemas';
/**
* Checks if all processing statuses are completed (not in "Bestellt" state).
* Returns true if all statuses are different from "Bestellt" (16).
* Returns false if any status is still "Bestellt" (16) or if the array is empty/undefined.
*/
export const getProcessingStatusCompleted = (
statuses: number[] | undefined,
): boolean => {
if (!statuses || statuses.length === 0) {
return false;
}
return statuses.every(
(status) => status !== OrderItemProcessingStatusValue.Bestellt,
);
};

View File

@@ -0,0 +1 @@
export * from './get-processing-status-completed.helper';

View File

@@ -6,3 +6,4 @@ export * from './questions';
export * from './schemas';
export * from './services';
export * from './stores';
export * from './resources';

View File

@@ -0,0 +1 @@
export * from './order-item-subset.resource';

View File

@@ -0,0 +1,51 @@
import { computed, inject, Injectable, resource, signal } from '@angular/core';
import { DisplayOrderItemSubset } from '../schemas';
import { OrderRewardCollectFacade } from '../facades';
@Injectable()
export class OrderItemSubsetResource {
#orderRewardCollectFacade = inject(OrderRewardCollectFacade);
#orderItemSubsetIds = signal<number[] | undefined>(undefined);
#orderItemSubsetsResource = resource({
params: computed(() => ({
orderItemSubsetIds: this.#orderItemSubsetIds(),
})),
loader: async ({
params,
abortSignal,
}): Promise<DisplayOrderItemSubset[]> => {
if (!params?.orderItemSubsetIds?.length) {
return [];
}
const results: DisplayOrderItemSubset[] = [];
for (const id of params.orderItemSubsetIds) {
const result = await this.#orderRewardCollectFacade.getOrderItemSubset(
{ orderItemSubsetId: id },
abortSignal,
);
if (result) {
results.push(result);
}
}
return results;
},
defaultValue: [],
});
readonly orderItemSubsets = this.#orderItemSubsetsResource.value.asReadonly();
readonly loading = this.#orderItemSubsetsResource.isLoading;
readonly error = computed(
() => this.#orderItemSubsetsResource.error()?.message ?? null,
);
loadOrderItemSubsets(orderItemSubsetIds: number[] | undefined) {
this.#orderItemSubsetIds.set(orderItemSubsetIds);
}
refresh() {
this.#orderItemSubsetsResource.reload();
}
}

View File

@@ -0,0 +1,51 @@
import { EntitySchema, DateRangeSchema } from '@isa/common/data-access';
import { z } from 'zod';
import { OrderItemProcessingStatusValueSchema } from './order-item-processing-status-value.schema';
// Forward declaration for circular reference
export const DisplayOrderItemSubsetSchema = z
.object({
compartmentCode: z.string().describe('Compartment code').optional(),
compartmentInfo: z.string().describe('Compartment information').optional(),
compartmentStart: z.string().describe('Compartment start').optional(),
compartmentStop: z.string().describe('Compartment stop').optional(),
description: z.string().describe('Description text').optional(),
estimatedDelivery: DateRangeSchema.describe(
'Estimated delivery date range',
).optional(),
estimatedShippingDate: z
.string()
.describe('Estimated shipping date')
.optional(),
orderItem: z
.lazy(() => z.any())
.describe('Order item')
.optional(), // Circular reference to DisplayOrderItem
orderItemSubsetNumber: z
.string()
.describe('Order item subset number')
.optional(),
preferredPickUpDate: z
.string()
.describe('Preferred pick up date')
.optional(),
processingStatus:
OrderItemProcessingStatusValueSchema.describe('Processing status'),
processingStatusDate: z
.string()
.describe('Processing status date')
.optional(),
quantity: z.number().describe('Quantity').optional(),
specialComment: z.string().describe('Special comment').optional(),
ssc: z.string().describe('SSC code').optional(),
sscText: z.string().describe('SSC text').optional(),
supplierLabel: z.string().describe('Supplier label').optional(),
supplierName: z.string().describe('Supplier name').optional(),
supplyChannel: z.string().describe('Supply channel').optional(),
trackingNumber: z.string().describe('Tracking number').optional(),
})
.extend(EntitySchema.shape);
export type DisplayOrderItemSubset = z.infer<
typeof DisplayOrderItemSubsetSchema
>;

View File

@@ -1,32 +1,33 @@
import { EntitySchema, QuantityUnitTypeSchema } from '@isa/common/data-access';
import { z } from 'zod';
import { PriceSchema } from './price.schema';
import { ProductSchema } from './product.schema';
import { PromotionSchema } from './promotion.schema';
// Forward declaration for circular reference
export const DisplayOrderItemSchema = z
.object({
buyerComment: z.string().describe('Buyer comment').optional(),
description: z.string().describe('Description text').optional(),
features: z.record(z.string().describe('Features'), z.string()).optional(),
order: z
.lazy(() => z.any())
.describe('Order')
.optional(), // Circular reference to DisplayOrder
orderDate: z.string().describe('Order date').optional(),
orderItemNumber: z.string().describe('OrderItem number').optional(),
price: PriceSchema.describe('Price information').optional(),
product: ProductSchema.describe('Product').optional(),
promotion: PromotionSchema.describe('Promotion information').optional(),
quantity: z.number().describe('Quantity').optional(),
quantityUnit: z.string().describe('Quantity unit').optional(),
quantityUnitType: QuantityUnitTypeSchema.describe('QuantityUnit type'),
subsetItems: z
.array(z.lazy(() => z.any()))
.describe('Subset items')
.optional(), // Circular reference to DisplayOrderItemSubset
})
.extend(EntitySchema.shape);
export type DisplayOrderItem = z.infer<typeof DisplayOrderItemSchema>;
import { EntitySchema, QuantityUnitTypeSchema } from '@isa/common/data-access';
import { z } from 'zod';
import { PriceSchema } from './price.schema';
import { ProductSchema } from './product.schema';
import { PromotionSchema } from './promotion.schema';
import { DisplayOrderItemSubsetSchema } from './display-order-item-subset.schema';
// Forward declaration for circular reference
export const DisplayOrderItemSchema = z
.object({
buyerComment: z.string().describe('Buyer comment').optional(),
description: z.string().describe('Description text').optional(),
features: z.record(z.string().describe('Features'), z.string()).optional(),
order: z
.lazy(() => z.any())
.describe('Order')
.optional(), // Circular reference to DisplayOrder
orderDate: z.string().describe('Order date').optional(),
orderItemNumber: z.string().describe('OrderItem number').optional(),
price: PriceSchema.describe('Price information').optional(),
product: ProductSchema.describe('Product').optional(),
promotion: PromotionSchema.describe('Promotion information').optional(),
quantity: z.number().describe('Quantity').optional(),
quantityUnit: z.string().describe('Quantity unit').optional(),
quantityUnitType: QuantityUnitTypeSchema.describe('QuantityUnit type'),
subsetItems: z
.array(DisplayOrderItemSubsetSchema)
.describe('Subset items')
.optional(),
})
.extend(EntitySchema.shape);
export type DisplayOrderItem = z.infer<typeof DisplayOrderItemSchema>;

View File

@@ -0,0 +1,9 @@
import z from 'zod';
export const FetchOrderItemSubsetSchema = z.object({
orderItemSubsetId: z.number(),
});
export type FetchOrderItemSubsetSchemaInput = z.infer<
typeof FetchOrderItemSubsetSchema
>;

View File

@@ -17,3 +17,8 @@ export * from './return-receipt-values.schema';
export * from './shipping-type.schema';
export * from './terms-of-delivery.schema';
export * from './type-of-delivery.schema';
export * from './loyalty-collect-type.schema';
export * from './order-loyalty-collect.schema';
export * from './fetch-order-item-subset.schema';
export * from './display-order-item-subset.schema';
export * from './order-item-processing-status-value.schema';

View File

@@ -0,0 +1,21 @@
import z from 'zod';
export const LoyaltyCollectType = {
Collect: 0,
OutOfStock: 1,
Cancel: 2,
} as const;
const ALL_FLAGS = Object.values(LoyaltyCollectType).reduce<number>(
(a, b) => a | b,
0,
);
export const LoyaltyCollectTypeSchema = z
.nativeEnum(LoyaltyCollectType)
.refine((val) => (val & ALL_FLAGS) === val, {
message: 'Invalid loyalty collect type',
})
.describe('Loyalty collect type');
export type LoyaltyCollectType = z.infer<typeof LoyaltyCollectTypeSchema>;

View File

@@ -0,0 +1,51 @@
import z from 'zod';
export const OrderItemProcessingStatusValue = {
NotSet: 0,
NeuAngelegt1: 1,
NeuAngelegt2: 2,
NeuÜbernommen: 4,
Geparkt: 8,
Bestellt: 16,
VorbereitungVersand: 32,
Versendet: 64,
Eingetroffen: 128,
Abgeholt: 256,
StorniertKunde: 512,
Storniert: 1024,
StorniertLieferant: 2048,
NichtLieferbar: 4096,
Nachbestellt: 8192,
Zurückgegeben: 16384,
ZumDownloadVerfügbar: 32768,
Downloaded: 65536,
NichtAbgeholt: 131072,
AnsLagerNichtAbgeholt: 262144,
Angefragt: 524288,
WeitergeleitetIntern: 1048576,
Überfällig: 2097152,
Zugestellt: 4194304,
LieferantErmittelt: 8388608,
DerzeitNichtLieferbar: 16777216,
Reserviert: 33554432,
Zusammengestellt: 67108864,
Verpackt: 134217728,
Lieferschein: 268435456,
} as const;
export const OrderItemProcessingStatusValueSchema = z.number().refine(
(val) => {
// Validate that the value is a valid combination of flags
const allFlags = Object.values(
OrderItemProcessingStatusValue,
).reduce<number>((acc, flag) => acc | flag, 0);
return (val & allFlags) === val;
},
{
message: 'Invalid order item processing status value',
},
);
export type OrderItemProcessingStatusValue = z.infer<
typeof OrderItemProcessingStatusValueSchema
>;

View File

@@ -0,0 +1,14 @@
import z from 'zod';
import { LoyaltyCollectTypeSchema } from './loyalty-collect-type.schema';
export const OrderLoyaltyCollectSchema = z.object({
orderId: z.number(),
orderItemId: z.number(),
orderItemSubsetId: z.number(),
collectType: LoyaltyCollectTypeSchema,
quantity: z.number().int().nonnegative(),
});
export type OrderLoyaltyCollectInput = z.infer<
typeof OrderLoyaltyCollectSchema
>;

View File

@@ -8,3 +8,4 @@ export * from './return-details.service';
export * from './return-process.service';
export * from './return-search.service';
export * from './return-task-list.service';
export * from './order-reward-collect.service';

View File

@@ -0,0 +1,111 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { OrderRewardCollectService } from './order-reward-collect.service';
import {
OrderService,
DBHOrderItemListItemDTO,
} from '@generated/swagger/oms-api';
import { of } from 'rxjs';
describe('OrderRewardCollectService', () => {
let spectator: SpectatorService<OrderRewardCollectService>;
const createService = createServiceFactory({
service: OrderRewardCollectService,
mocks: [OrderService],
});
beforeEach(() => {
spectator = createService();
});
it('should be created', () => {
expect(spectator.service).toBeTruthy();
});
describe('collect', () => {
it('should collect reward item successfully', async () => {
// Arrange
const mockParams = {
orderId: 123,
orderItemId: 456,
orderItemSubsetId: 789,
collectType: 1 as const, // Valid LoyaltyCollectType.Collect
quantity: 2,
};
const mockResult: DBHOrderItemListItemDTO[] = [
{ orderItemType: 1 } as DBHOrderItemListItemDTO,
];
const mockResponse = { result: mockResult, error: false };
const orderService = spectator.inject(OrderService);
orderService.OrderLoyaltyCollect.mockReturnValue(of(mockResponse));
// Act
const result = await spectator.service.collect(mockParams);
// Assert
expect(result).toEqual(mockResult);
expect(orderService.OrderLoyaltyCollect).toHaveBeenCalledWith({
orderId: mockParams.orderId,
orderItemId: mockParams.orderItemId,
orderItemSubsetId: mockParams.orderItemSubsetId,
data: {
collectType: mockParams.collectType,
quantity: mockParams.quantity,
},
});
expect(orderService.OrderLoyaltyCollect).toHaveBeenCalledTimes(1);
});
it('should throw error if API response contains error', async () => {
// Arrange
const mockParams = {
orderId: 123,
orderItemId: 456,
orderItemSubsetId: 789,
collectType: 1 as const, // Valid LoyaltyCollectType.Collect
quantity: 2,
};
const mockResponse = { error: true, result: undefined };
const orderService = spectator.inject(OrderService);
orderService.OrderLoyaltyCollect.mockReturnValue(of(mockResponse));
// Act & Assert
await expect(spectator.service.collect(mockParams)).rejects.toThrow();
expect(orderService.OrderLoyaltyCollect).toHaveBeenCalledWith({
orderId: mockParams.orderId,
orderItemId: mockParams.orderItemId,
orderItemSubsetId: mockParams.orderItemSubsetId,
data: {
collectType: mockParams.collectType,
quantity: mockParams.quantity,
},
});
});
});
describe('fetchOrderItemSubset', () => {
it('should fetch order item subset successfully', async () => {
// Arrange
const mockParams = {
orderItemSubsetId: 789,
};
const mockResult = {
id: 789,
quantity: 2,
name: 'Test Subset',
};
const mockResponse = { result: mockResult, error: false };
const orderService = spectator.inject(OrderService);
orderService.OrderGetOrderItemSubset.mockReturnValue(of(mockResponse));
// Act
const result = await spectator.service.fetchOrderItemSubset(mockParams);
// Assert
expect(result).toEqual(mockResult);
expect(orderService.OrderGetOrderItemSubset).toHaveBeenCalledWith(
mockParams.orderItemSubsetId,
);
expect(orderService.OrderGetOrderItemSubset).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,77 @@
import { inject, Injectable } from '@angular/core';
import { OrderService } from '@generated/swagger/oms-api';
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
import { logger } from '@isa/core/logging';
import { firstValueFrom } from 'rxjs';
import {
DisplayOrderItemSubset,
FetchOrderItemSubsetSchema,
FetchOrderItemSubsetSchemaInput,
OrderLoyaltyCollectInput,
OrderLoyaltyCollectSchema,
} from '../schemas';
@Injectable({ providedIn: 'root' })
export class OrderRewardCollectService {
#logger = logger(() => ({ service: 'OrderRewardCollectService' }));
#orderService = inject(OrderService);
async collect(params: OrderLoyaltyCollectInput) {
try {
params = OrderLoyaltyCollectSchema.parse(params);
} catch (error) {
this.#logger.error('Failed to parse schema', error);
throw error;
}
const req$ = this.#orderService.OrderLoyaltyCollect({
orderId: params.orderId,
orderItemId: params.orderItemId,
orderItemSubsetId: params.orderItemSubsetId,
data: {
collectType: params.collectType,
quantity: params.quantity,
},
});
const res = await firstValueFrom(req$);
if (res.error) {
const error = new ResponseArgsError(res);
this.#logger.error('Failed to collect reward item', error);
throw error;
}
return res.result;
}
async fetchOrderItemSubset(
params: FetchOrderItemSubsetSchemaInput,
abortSignal?: AbortSignal,
) {
try {
params = FetchOrderItemSubsetSchema.parse(params);
} catch (error) {
this.#logger.error('Failed to parse schema', error);
throw error;
}
let req$ = this.#orderService.OrderGetOrderItemSubset(
params.orderItemSubsetId,
);
if (abortSignal) {
req$ = req$.pipe(takeUntilAborted(abortSignal));
}
const res = await firstValueFrom(req$);
if (res.error) {
const error = new ResponseArgsError(res);
this.#logger.error('Failed to fetch order item subset', error);
throw error;
}
return res.result as DisplayOrderItemSubset;
}
}

567
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff