mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 2009: fix shopping cart - sync issues
This commit is contained in:
committed by
Nino Righi
parent
f04e36e710
commit
731df8414d
@@ -72,12 +72,82 @@ import { ApplicationService } from '@core/application';
|
||||
import { CustomerDTO } from '@generated/swagger/crm-api';
|
||||
import { Config } from '@core/config';
|
||||
import parseDuration from 'parse-duration';
|
||||
import { CheckoutMetadataService } from '@isa/checkout/data-access';
|
||||
import {
|
||||
CheckoutMetadataService,
|
||||
ShoppingCart,
|
||||
} from '@isa/checkout/data-access';
|
||||
import {
|
||||
ShoppingCartEvent,
|
||||
ShoppingCartEvents,
|
||||
} from '@isa/checkout/data-access';
|
||||
|
||||
/**
|
||||
* Domain service for managing the complete checkout flow including shopping cart operations,
|
||||
* checkout creation, buyer/payer management, payment processing, and order completion.
|
||||
*
|
||||
* This service orchestrates interactions between:
|
||||
* - NgRx Store for state management
|
||||
* - Multiple Swagger API clients (checkout, OMS, shopping cart, payment, buyer, payer, branch, kulturpass)
|
||||
* - Shopping cart event system for cross-component synchronization
|
||||
* - Availability service for real-time product availability checks
|
||||
*
|
||||
* @remarks
|
||||
* **Process ID Pattern**: All methods require a `processId` (typically `Date.now()`) to isolate
|
||||
* checkout sessions. Multiple concurrent checkout processes can run independently.
|
||||
*
|
||||
* **Observable-First Design**: Most methods return Observables for reactive composition. Consumers
|
||||
* should use RxJS operators for transformation and error handling.
|
||||
*
|
||||
* **Auto-Creation**: Shopping carts auto-create if missing. The service uses `filter()` operators
|
||||
* to trigger lazy initialization and prevent race conditions.
|
||||
*
|
||||
* **Event Sourcing**: Publishes shopping cart events (Created, ItemAdded, ItemUpdated, ItemRemoved)
|
||||
* for synchronization across components. Subscribes to events from `ShoppingCartService` to maintain
|
||||
* consistency.
|
||||
*
|
||||
* **OLA Management**: Tracks Order Level Agreement (OLA) expiration timestamps per item and order type.
|
||||
* Validates availability freshness before checkout completion.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic shopping cart flow
|
||||
* const processId = Date.now();
|
||||
*
|
||||
* // Add items to cart
|
||||
* this.checkoutService.addItemToShoppingCart({
|
||||
* processId,
|
||||
* items: [{ productId: 123, quantity: 2 }]
|
||||
* }).subscribe();
|
||||
*
|
||||
* // Get cart (auto-creates if missing)
|
||||
* this.checkoutService.getShoppingCart({ processId })
|
||||
* .subscribe(cart => console.log(cart));
|
||||
*
|
||||
* // Complete checkout (orchestrates all steps)
|
||||
* this.checkoutService.completeCheckout({ processId })
|
||||
* .subscribe(orders => console.log('Orders created:', orders));
|
||||
* ```
|
||||
*
|
||||
* @see DomainCheckoutSelectors For state selection patterns
|
||||
* @see DomainCheckoutActions For available actions
|
||||
* @see ShoppingCartEvents For event system integration
|
||||
*/
|
||||
@Injectable()
|
||||
export class DomainCheckoutService {
|
||||
/** Metadata service for shopping cart persistence */
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
|
||||
/** Event bus for shopping cart synchronization across components */
|
||||
#shoppingCartEvents = inject(ShoppingCartEvents);
|
||||
|
||||
/**
|
||||
* Gets the OLA (Order Level Agreement) expiration duration in milliseconds.
|
||||
*
|
||||
* OLA expiration defines how long availability data remains valid before requiring refresh.
|
||||
* Default is 5 minutes if not configured.
|
||||
*
|
||||
* @returns Duration in milliseconds
|
||||
*/
|
||||
get olaExpiration() {
|
||||
const exp = this._config.get('@domain/checkout.olaExpiration') ?? '5m';
|
||||
return parseDuration(exp);
|
||||
@@ -96,9 +166,56 @@ export class DomainCheckoutService {
|
||||
private _payerService: StoreCheckoutPayerService,
|
||||
private _branchService: StoreCheckoutBranchService,
|
||||
private _kulturpassService: KulturPassService,
|
||||
) {}
|
||||
) {
|
||||
// Subscribe to shopping cart events from ShoppingCartService
|
||||
this.#shoppingCartEvents.events$
|
||||
.pipe(
|
||||
// Only process events from ShoppingCartService to avoid circular updates
|
||||
filter((payload) => payload.source === 'ShoppingCartService'),
|
||||
)
|
||||
.subscribe((payload) => {
|
||||
// Update the store with the shopping cart from the event
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.setShoppingCartByShoppingCartId({
|
||||
shoppingCartId: payload.shoppingCart.id!,
|
||||
shoppingCart: payload.shoppingCart as any,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//#region shoppingcart
|
||||
/**
|
||||
* Retrieves the shopping cart for a given process ID. Auto-creates the cart if it doesn't exist.
|
||||
*
|
||||
* @remarks
|
||||
* **Auto-Creation**: If no cart exists for the process ID, triggers `createShoppingCart()` automatically.
|
||||
* The Observable filters out null/undefined and waits for cart creation to complete.
|
||||
*
|
||||
* **Latest Data**: Setting `latest: true` forces a refresh from the API instead of using cached state.
|
||||
* This is useful before critical operations like checkout completion.
|
||||
*
|
||||
* **Memoization**: Uses `@memorize()` decorator to cache results by parameters, reducing duplicate calls.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Unique process identifier (typically `Date.now()`)
|
||||
* @param params.latest - If true, fetches fresh data from API; if false/undefined, uses store state
|
||||
*
|
||||
* @returns Observable of the shopping cart DTO. Never emits null/undefined.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get cart from store (creates if missing)
|
||||
* this.checkoutService.getShoppingCart({ processId: 123 })
|
||||
* .pipe(first())
|
||||
* .subscribe(cart => console.log('Items:', cart.items));
|
||||
*
|
||||
* // Force refresh from API
|
||||
* this.checkoutService.getShoppingCart({ processId: 123, latest: true })
|
||||
* .pipe(first())
|
||||
* .subscribe(cart => console.log('Fresh cart:', cart));
|
||||
* ```
|
||||
*/
|
||||
@memorize()
|
||||
getShoppingCart({
|
||||
processId,
|
||||
@@ -143,6 +260,24 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the shopping cart from the API and updates the store with fresh data.
|
||||
*
|
||||
* This is an async method that fetches the latest cart state from the backend
|
||||
* and dispatches an action to update the NgRx store. Unlike `getShoppingCart({ latest: true })`,
|
||||
* this method doesn't return the cart Observable - it's fire-and-forget.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
*
|
||||
* @returns Promise that resolves when reload completes (or immediately if no cart exists)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await this.checkoutService.reloadShoppingCart({ processId: 123 });
|
||||
* console.log('Cart reloaded');
|
||||
* ```
|
||||
*/
|
||||
async reloadShoppingCart({ processId }: { processId: number }) {
|
||||
const shoppingCart = await firstValueFrom(
|
||||
this.store.select(DomainCheckoutSelectors.selectShoppingCartByProcessId, {
|
||||
@@ -166,6 +301,29 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new shopping cart and associates it with the given process ID.
|
||||
*
|
||||
* @remarks
|
||||
* **State Updates**:
|
||||
* - Saves shopping cart ID to metadata service for persistence
|
||||
* - Publishes `ShoppingCartEvent.Created` event for component synchronization
|
||||
* - Dispatches `setShoppingCart` action to update NgRx store
|
||||
*
|
||||
* **Auto-Invocation**: Usually called automatically by `getShoppingCart()` when no cart exists.
|
||||
* Rarely needs to be called directly.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier to associate with the new cart
|
||||
*
|
||||
* @returns Observable of the newly created shopping cart DTO
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.createShoppingCart({ processId: Date.now() })
|
||||
* .subscribe(cart => console.log('Cart created:', cart.id));
|
||||
* ```
|
||||
*/
|
||||
createShoppingCart({
|
||||
processId,
|
||||
}: {
|
||||
@@ -180,6 +338,11 @@ export class DomainCheckoutService {
|
||||
processId,
|
||||
shoppingCart.id,
|
||||
);
|
||||
this.#shoppingCartEvents.pub(
|
||||
ShoppingCartEvent.Created,
|
||||
shoppingCart as ShoppingCart,
|
||||
'DomainCheckoutService',
|
||||
);
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.setShoppingCart({
|
||||
processId,
|
||||
@@ -190,6 +353,41 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more items to the shopping cart.
|
||||
*
|
||||
* @remarks
|
||||
* **Process Flow**:
|
||||
* 1. Retrieves existing shopping cart (creates if missing)
|
||||
* 2. Calls API to add items
|
||||
* 3. Publishes `ShoppingCartEvent.ItemAdded` event
|
||||
* 4. Updates NgRx store with modified cart
|
||||
*
|
||||
* **Validation**: Ensure items are validated via `canAddItem()` or `canAddItems()` before calling
|
||||
* this method to avoid API errors.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
* @param params.items - Array of items to add (product, quantity, availability, destination, etc.)
|
||||
*
|
||||
* @returns Observable of the updated shopping cart with new items
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.addItemToShoppingCart({
|
||||
* processId: 123,
|
||||
* items: [{
|
||||
* product: { ean: '1234567890', catalogProductNumber: 456 },
|
||||
* quantity: 2,
|
||||
* availability: availabilityDto,
|
||||
* destination: destinationDto
|
||||
* }]
|
||||
* }).subscribe(cart => console.log('Items:', cart.items.length));
|
||||
* ```
|
||||
*
|
||||
* @see canAddItem For single item validation
|
||||
* @see canAddItems For bulk item validation
|
||||
*/
|
||||
addItemToShoppingCart({
|
||||
processId,
|
||||
items,
|
||||
@@ -208,6 +406,11 @@ export class DomainCheckoutService {
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
tap((shoppingCart) => {
|
||||
this.#shoppingCartEvents.pub(
|
||||
ShoppingCartEvent.ItemAdded,
|
||||
shoppingCart as ShoppingCart,
|
||||
'DomainCheckoutService',
|
||||
);
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.setShoppingCart({
|
||||
processId,
|
||||
@@ -267,6 +470,37 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a single item can be added to the shopping cart.
|
||||
*
|
||||
* Checks business rules, customer features, and cart compatibility before adding items.
|
||||
* Use this before calling `addItemToShoppingCart()` to prevent API errors.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
* @param params.availability - OLA availability data for the item
|
||||
* @param params.orderType - Order type (e.g., 'Abholung', 'Versand', 'Download', 'Rücklage')
|
||||
*
|
||||
* @returns Observable of `true` if item can be added, or error message string if not allowed
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.canAddItem({
|
||||
* processId: 123,
|
||||
* availability: olaAvailability,
|
||||
* orderType: 'Versand'
|
||||
* }).subscribe(result => {
|
||||
* if (result === true) {
|
||||
* // Proceed with adding item
|
||||
* this.checkoutService.addItemToShoppingCart(...);
|
||||
* } else {
|
||||
* console.error('Cannot add item:', result);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see canAddItems For bulk validation
|
||||
*/
|
||||
canAddItem({
|
||||
processId,
|
||||
availability,
|
||||
@@ -312,6 +546,38 @@ export class DomainCheckoutService {
|
||||
.pipe(map((response) => response?.result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if multiple items can be added to the shopping cart in bulk.
|
||||
*
|
||||
* More efficient than calling `canAddItem()` multiple times. Returns validation
|
||||
* results for each item in the payload.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
* @param params.payload - Array of item payloads to validate
|
||||
* @param params.orderType - Order type for all items
|
||||
*
|
||||
* @returns Observable array of validation results (one per item). Each result contains
|
||||
* `ok` flag and optional error messages.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.canAddItems({
|
||||
* processId: 123,
|
||||
* payload: [
|
||||
* { availabilities: [avail1], productId: 111, quantity: 2 },
|
||||
* { availabilities: [avail2], productId: 222, quantity: 1 }
|
||||
* ],
|
||||
* orderType: 'Versand'
|
||||
* }).subscribe(results => {
|
||||
* results.forEach((result, index) => {
|
||||
* console.log(`Item ${index}:`, result.ok ? 'Valid' : result.message);
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see canAddItem For single item validation
|
||||
*/
|
||||
canAddItems({
|
||||
processId,
|
||||
payload,
|
||||
@@ -383,6 +649,50 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing item in the shopping cart (quantity, availability, special comment, etc.).
|
||||
*
|
||||
* @remarks
|
||||
* **Special Behavior**:
|
||||
* - Setting `quantity: 0` removes the item and publishes `ItemRemoved` event instead of `ItemUpdated`
|
||||
* - Always fetches latest cart state (`latest: true`) to avoid stale data conflicts
|
||||
* - If availability is updated, adds timestamp to history for OLA validation
|
||||
*
|
||||
* **Event Publishing**:
|
||||
* - Publishes `ShoppingCartEvent.ItemRemoved` if quantity is 0
|
||||
* - Publishes `ShoppingCartEvent.ItemUpdated` for all other changes
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
* @param params.shoppingCartItemId - ID of the cart item to update
|
||||
* @param params.update - Fields to update (quantity, availability, specialComment, etc.)
|
||||
*
|
||||
* @returns Observable of the updated shopping cart
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Update quantity
|
||||
* this.checkoutService.updateItemInShoppingCart({
|
||||
* processId: 123,
|
||||
* shoppingCartItemId: 456,
|
||||
* update: { quantity: 5 }
|
||||
* }).subscribe();
|
||||
*
|
||||
* // Remove item (quantity = 0)
|
||||
* this.checkoutService.updateItemInShoppingCart({
|
||||
* processId: 123,
|
||||
* shoppingCartItemId: 456,
|
||||
* update: { quantity: 0 }
|
||||
* }).subscribe();
|
||||
*
|
||||
* // Update availability
|
||||
* this.checkoutService.updateItemInShoppingCart({
|
||||
* processId: 123,
|
||||
* shoppingCartItemId: 456,
|
||||
* update: { availability: newAvailabilityDto }
|
||||
* }).subscribe();
|
||||
* ```
|
||||
*/
|
||||
updateItemInShoppingCart({
|
||||
processId,
|
||||
shoppingCartItemId,
|
||||
@@ -404,6 +714,17 @@ export class DomainCheckoutService {
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
tap((shoppingCart) => {
|
||||
// Check if item was removed (quantity === 0)
|
||||
const eventType =
|
||||
update.quantity === 0
|
||||
? ShoppingCartEvent.ItemRemoved
|
||||
: ShoppingCartEvent.ItemUpdated;
|
||||
this.#shoppingCartEvents.pub(
|
||||
eventType,
|
||||
shoppingCart as ShoppingCart,
|
||||
'DomainCheckoutService',
|
||||
);
|
||||
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.setShoppingCart({
|
||||
processId,
|
||||
@@ -432,6 +753,34 @@ export class DomainCheckoutService {
|
||||
|
||||
//#region Checkout
|
||||
|
||||
/**
|
||||
* Retrieves the checkout entity for a given process. Auto-creates if missing.
|
||||
*
|
||||
* @remarks
|
||||
* **Auto-Creation**: Similar to `getShoppingCart()`, automatically triggers `createCheckout()`
|
||||
* if no checkout exists for the process ID.
|
||||
*
|
||||
* **Refresh**: Setting `refresh: true` forces recreation of the checkout entity from the API.
|
||||
*
|
||||
* **Purpose**: The checkout entity aggregates buyer, payer, payment, destinations, and
|
||||
* notification channels. It's required before order completion.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
* @param params.refresh - If true, recreates checkout from API; if false/undefined, uses store state
|
||||
*
|
||||
* @returns Observable of the checkout DTO. Never emits null/undefined.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.getCheckout({ processId: 123 })
|
||||
* .pipe(first())
|
||||
* .subscribe(checkout => {
|
||||
* console.log('Buyer:', checkout.buyer);
|
||||
* console.log('Payment:', checkout.payment);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getCheckout({
|
||||
processId,
|
||||
refresh,
|
||||
@@ -772,6 +1121,40 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the availability data for a single shopping cart item.
|
||||
*
|
||||
* Fetches fresh availability from the appropriate service based on order type
|
||||
* (Abholung, Rücklage, Download, Versand, DIG-Versand, B2B-Versand) and updates
|
||||
* the cart item.
|
||||
*
|
||||
* @remarks
|
||||
* **Order Type Handling**:
|
||||
* - **Abholung** (Pickup): Requires branch for availability check
|
||||
* - **Rücklage** (TakeAway): Requires branch for availability check
|
||||
* - **Download**: No additional parameters needed
|
||||
* - **Versand** (Delivery): Standard delivery availability
|
||||
* - **DIG-Versand** (Digital Delivery): Digital goods delivery
|
||||
* - **B2B-Versand** (B2B Delivery): Business customer delivery
|
||||
*
|
||||
* **Updates**: Automatically calls `updateItemInShoppingCart()` with the new availability
|
||||
* after fetching.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
* @param params.shoppingCartItemId - ID of the cart item to refresh
|
||||
*
|
||||
* @returns Promise of the refreshed availability DTO, or undefined if item not found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const availability = await this.checkoutService.refreshAvailability({
|
||||
* processId: 123,
|
||||
* shoppingCartItemId: 456
|
||||
* });
|
||||
* console.log('Updated availability:', availability);
|
||||
* ```
|
||||
*/
|
||||
async refreshAvailability({
|
||||
processId,
|
||||
shoppingCartItemId,
|
||||
@@ -876,9 +1259,41 @@ export class DomainCheckoutService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the availability of all items is valid
|
||||
* @param param0 Process Id
|
||||
* @returns true if the availability of all items is valid
|
||||
* Validates if all shopping cart items have fresh availability data (OLA not expired).
|
||||
*
|
||||
* OLA (Order Level Agreement) defines how long availability data remains valid.
|
||||
* This method polls the store at regular intervals and checks if the oldest
|
||||
* availability timestamp is still within the expiration window.
|
||||
*
|
||||
* @remarks
|
||||
* **Polling**: Uses `rxjsInterval()` to continuously check OLA status. The Observable emits
|
||||
* `true` while all items are valid, `false` when any item expires.
|
||||
*
|
||||
* **Timestamp Tracking**: Tracks timestamps per `${itemId}_${orderType}` combination.
|
||||
* Shipping types (Versand, DIG-Versand, B2B-Versand) fall back to generic 'Versand' timestamp.
|
||||
*
|
||||
* **Default Interval**: Polls every `olaExpiration / 10` milliseconds (default: 30 seconds for 5-minute expiration).
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
* @param params.interval - Custom polling interval in milliseconds (optional)
|
||||
*
|
||||
* @returns Observable that emits `true` when OLA is valid, `false` when expired.
|
||||
* Emits only on changes (`distinctUntilChanged()`).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.validateOlaStatus({ processId: 123 })
|
||||
* .subscribe(isValid => {
|
||||
* if (!isValid) {
|
||||
* console.warn('Availability data expired! Refresh required.');
|
||||
* // Trigger availability refresh
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see olaExpiration For expiration duration configuration
|
||||
* @see checkoutIsValid For combined OLA + availability validation
|
||||
*/
|
||||
validateOlaStatus({
|
||||
processId,
|
||||
@@ -965,6 +1380,39 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the checkout is ready for order completion.
|
||||
*
|
||||
* Combines OLA status validation with availability validation. Both must be true
|
||||
* for checkout to proceed.
|
||||
*
|
||||
* @remarks
|
||||
* **Validation Checks**:
|
||||
* - OLA Status: All item availabilities are within expiration window
|
||||
* - Availabilities: All items are marked as available (not out of stock)
|
||||
*
|
||||
* **Polling**: Uses fast polling (250ms) for OLA status to catch expiration quickly.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
*
|
||||
* @returns Observable that emits `true` when checkout is valid, `false` otherwise.
|
||||
* Emits on every change in either validation.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.checkoutIsValid({ processId: 123 })
|
||||
* .subscribe(isValid => {
|
||||
* this.checkoutButton.disabled = !isValid;
|
||||
* if (!isValid) {
|
||||
* this.showWarning('Checkout unavailable: Availability expired');
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see validateOlaStatus For OLA-only validation
|
||||
* @see validateAvailabilities For availability-only validation
|
||||
*/
|
||||
checkoutIsValid({ processId }: { processId: number }): Observable<boolean> {
|
||||
const olaStatus$ = this.validateOlaStatus({ processId, interval: 250 });
|
||||
|
||||
@@ -975,6 +1423,73 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Orchestrates the complete checkout process from cart validation to order creation.
|
||||
*
|
||||
* This is the most complex method in the service, executing a 12-step sequence that
|
||||
* validates data, updates entities, and creates orders. Each step must complete before
|
||||
* the next begins.
|
||||
*
|
||||
* @remarks
|
||||
* **Execution Sequence** (sequential, not parallel):
|
||||
* 1. **Update Destination**: Sets shipping addresses on delivery destinations
|
||||
* 2. **Refresh Shopping Cart**: Gets latest cart state from API
|
||||
* 3. **Set Special Comments**: Applies agent comments to all cart items
|
||||
* 4. **Refresh Checkout**: Recreates checkout entity from current state
|
||||
* 5. **Check Availabilities**: Validates download items are available
|
||||
* 6. **Update Availabilities**: Refreshes DIG-Versand and B2B-Versand prices
|
||||
* 7. **Set Buyer**: Submits buyer information to checkout
|
||||
* 8. **Set Notification Channels**: Configures email/SMS preferences
|
||||
* 9. **Set Payer**: Submits payer information (if needed for order type)
|
||||
* 10. **Set Payment Type**: Configures payment method (Rechnung/Bar)
|
||||
* 11. **Set Destination**: Updates destinations with shipping addresses
|
||||
* 12. **Complete Order**: Submits to OMS for order creation
|
||||
*
|
||||
* **Payment Type Logic**:
|
||||
* - Download/Versand/DIG-Versand/B2B-Versand → Payment type 128 (Rechnung/Invoice)
|
||||
* - Pickup/TakeAway only → Payment type 4 (Bar/Cash)
|
||||
*
|
||||
* **Payer Requirement**:
|
||||
* - Required for B2B customers or Download/Delivery order types
|
||||
* - Skipped for in-store pickup/takeaway only
|
||||
*
|
||||
* **Error Handling**:
|
||||
* - HTTP 409 (Conflict): Order already exists - dispatches existing orders to store
|
||||
* - Other errors propagate to consumer for handling
|
||||
* - Failed availability checks throw error preventing order creation
|
||||
*
|
||||
* **Side Effects**:
|
||||
* - Logs each step to console (for debugging)
|
||||
* - Updates NgRx store at multiple points
|
||||
* - Dispatches final orders to store on success
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
*
|
||||
* @returns Observable array of created orders (DisplayOrderDTO[])
|
||||
*
|
||||
* @throws Observable error if availability validation fails or API returns non-409 error
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.completeCheckout({ processId: 123 })
|
||||
* .subscribe({
|
||||
* next: (orders) => {
|
||||
* console.log('Orders created:', orders);
|
||||
* this.router.navigate(['/order-confirmation']);
|
||||
* },
|
||||
* error: (error) => {
|
||||
* if (error.status === 409) {
|
||||
* console.log('Order already exists');
|
||||
* } else {
|
||||
* console.error('Checkout failed:', error);
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see completeKulturpassOrder For Kulturpass-specific checkout flow
|
||||
*/
|
||||
completeCheckout({
|
||||
processId,
|
||||
}: {
|
||||
@@ -1328,11 +1843,45 @@ export class DomainCheckoutService {
|
||||
|
||||
//#region Common
|
||||
|
||||
// Fix für Ticket #4619 Versand Artikel im Warenkob -> keine Änderung bei Kundendaten erfassen
|
||||
// Auskommentiert, da dieser Aufruf oftmals mit gleichen Parametern aufgerufen wird (ohne ausgewählten Kunden nur ein leeres Objekt bei customerFeatures)
|
||||
// memorize macht keinen deepCompare von Objekten und denkt hier, dass immer der gleiche Return Wert zurückkommt, allerdings ist das hier oft nicht der Fall
|
||||
// und der Decorator memorized dann fälschlicherweise
|
||||
// @memorize()
|
||||
/**
|
||||
* Validates if a customer can be set on the shopping cart based on cart contents and customer features.
|
||||
*
|
||||
* @remarks
|
||||
* **Memoization Disabled**: The `@memorize()` decorator was intentionally disabled for this method
|
||||
* due to shallow comparison issues. The decorator couldn't detect when `customerFeatures` object
|
||||
* changed, causing stale cached results. See Ticket #4619.
|
||||
*
|
||||
* **Response Fields**:
|
||||
* - `ok`: True if customer can be set without issues
|
||||
* - `filter`: Customer search filters (e.g., `{ customertype: 'webshop;guest' }`)
|
||||
* - `message`: Error message if validation fails
|
||||
* - `create`: Options for creating new customer types (store, guest, webshop, b2b)
|
||||
*
|
||||
* **Use Cases**:
|
||||
* - Determine which customer types are compatible with cart contents
|
||||
* - Pre-filter customer search results
|
||||
* - Enable/disable customer type creation buttons
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier
|
||||
* @param params.customerFeatures - Customer feature flags (optional: webshop, guest, b2b, staff, etc.)
|
||||
*
|
||||
* @returns Observable with validation result containing ok, filter, message, and create options
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.canSetCustomer({
|
||||
* processId: 123,
|
||||
* customerFeatures: { webshop: 'true', guest: 'false' }
|
||||
* }).subscribe(result => {
|
||||
* if (result.ok) {
|
||||
* console.log('Customer types allowed:', result.create.options.values);
|
||||
* } else {
|
||||
* console.error('Cannot set customer:', result.message);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
canSetCustomer({
|
||||
processId,
|
||||
customerFeatures,
|
||||
@@ -1467,6 +2016,32 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all active, online branches that support shipping.
|
||||
*
|
||||
* @remarks
|
||||
* **Filtering**: Returns only branches matching ALL criteria:
|
||||
* - `status === 1` (Active)
|
||||
* - `branchType === 1` (Standard branch type)
|
||||
* - `isOnline === true` (Available online)
|
||||
* - `isShippingEnabled === true` (Supports shipping)
|
||||
*
|
||||
* **Memoization**: Uses `@memorize()` decorator to cache results. Subsequent calls
|
||||
* return cached data without API round-trip.
|
||||
*
|
||||
* **Pagination**: Fetches up to 999 branches (effectively all branches).
|
||||
*
|
||||
* @returns Observable array of filtered branch DTOs
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* this.checkoutService.getBranches()
|
||||
* .subscribe(branches => {
|
||||
* console.log('Available branches:', branches.length);
|
||||
* this.branchDropdown.options = branches;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@memorize()
|
||||
getBranches(): Observable<BranchDTO[]> {
|
||||
return this._branchService
|
||||
@@ -1547,6 +2122,21 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all checkout data for a process ID from the store.
|
||||
*
|
||||
* Cleans up shopping cart, checkout entity, buyer, payer, and all associated data.
|
||||
* Call this when a checkout session is complete or cancelled.
|
||||
*
|
||||
* @param params - Parameters object
|
||||
* @param params.processId - Process identifier to remove
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // After successful order or when user cancels
|
||||
* this.checkoutService.removeProcess({ processId: 123 });
|
||||
* ```
|
||||
*/
|
||||
removeProcess({ processId }: { processId: number }) {
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.removeCheckoutWithProcessId({ processId }),
|
||||
|
||||
@@ -19,6 +19,11 @@ export const setShoppingCart = createAction(
|
||||
props<{ processId: number; shoppingCart: ShoppingCartDTO }>(),
|
||||
);
|
||||
|
||||
export const setShoppingCartByShoppingCartId = createAction(
|
||||
`${prefix} Set Shopping Cart By Shopping Cart Id`,
|
||||
props<{ shoppingCartId: number; shoppingCart: ShoppingCartDTO }>(),
|
||||
);
|
||||
|
||||
export const setCheckout = createAction(
|
||||
`${prefix} Set Checkout`,
|
||||
props<{ processId: number; checkout: CheckoutDTO }>(),
|
||||
|
||||
@@ -1,207 +1,311 @@
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import { initialCheckoutState, storeCheckoutAdapter } from './domain-checkout.state';
|
||||
|
||||
import * as DomainCheckoutActions from './domain-checkout.actions';
|
||||
import { Dictionary } from '@ngrx/entity';
|
||||
import { CheckoutEntity } from './defs/checkout.entity';
|
||||
import { isNullOrUndefined } from '@utils/common';
|
||||
|
||||
const _domainCheckoutReducer = createReducer(
|
||||
initialCheckoutState,
|
||||
on(DomainCheckoutActions.setShoppingCart, (s, { processId, shoppingCart }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
|
||||
const addedShoppingCartItems =
|
||||
shoppingCart?.items
|
||||
?.filter((item) => !entity.shoppingCart?.items?.find((i) => i.id === item.id))
|
||||
?.map((item) => item.data) ?? [];
|
||||
|
||||
entity.shoppingCart = shoppingCart;
|
||||
|
||||
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ? { ...entity.itemAvailabilityTimestamp } : {};
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (let shoppingCartItem of addedShoppingCartItems) {
|
||||
if (shoppingCartItem.features?.orderType) {
|
||||
entity.itemAvailabilityTimestamp[`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`] = now;
|
||||
}
|
||||
}
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.checkout = checkout;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setBuyerCommunicationDetails, (s, { processId, email, mobile }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
const communicationDetails = { ...entity.buyer.communicationDetails };
|
||||
communicationDetails.email = email || communicationDetails.email;
|
||||
communicationDetails.mobile = mobile || communicationDetails.mobile;
|
||||
entity.buyer = {
|
||||
...entity.buyer,
|
||||
communicationDetails,
|
||||
};
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setNotificationChannels, (s, { processId, notificationChannels }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
return storeCheckoutAdapter.setOne({ ...entity, notificationChannels }, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCheckoutDestination, (s, { processId, destination }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.checkout = {
|
||||
...entity.checkout,
|
||||
destinations: entity.checkout.destinations.map((dest) => {
|
||||
if (dest.id === destination.id) {
|
||||
return { ...dest, ...destination };
|
||||
}
|
||||
return { ...dest };
|
||||
}),
|
||||
};
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setShippingAddress, (s, { processId, shippingAddress }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.shippingAddress = shippingAddress;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setBuyer, (s, { processId, buyer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.buyer = buyer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setPayer, (s, { processId, payer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.payer = payer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setSpecialComment, (s, { processId, agentComment }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.specialComment = agentComment;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.removeCheckoutWithProcessId, (s, { processId }) => {
|
||||
return storeCheckoutAdapter.removeOne(processId, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({ ...s, orders: [...s.orders, ...orders] })),
|
||||
on(DomainCheckoutActions.updateOrderItem, (s, { item }) => {
|
||||
const orders = [...s.orders];
|
||||
|
||||
const orderToUpdate = orders?.find((order) => order.items?.find((i) => i.id === item?.id));
|
||||
const orderToUpdateIndex = orders?.indexOf(orderToUpdate);
|
||||
|
||||
const orderItemToUpdate = orderToUpdate?.items?.find((i) => i.id === item?.id);
|
||||
const orderItemToUpdateIndex = orderToUpdate?.items?.indexOf(orderItemToUpdate);
|
||||
|
||||
const items = [...orderToUpdate?.items];
|
||||
items[orderItemToUpdateIndex] = item;
|
||||
|
||||
orders[orderToUpdateIndex] = {
|
||||
...orderToUpdate,
|
||||
items: [...items],
|
||||
};
|
||||
|
||||
return { ...s, orders: [...orders] };
|
||||
}),
|
||||
on(DomainCheckoutActions.removeAllOrders, (s) => ({
|
||||
...s,
|
||||
orders: [],
|
||||
})),
|
||||
on(DomainCheckoutActions.setOlaError, (s, { processId, olaErrorIds }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.olaErrorIds = olaErrorIds;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCustomer, (s, { processId, customer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.customer = customer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory,
|
||||
(s, { processId, shoppingCartItemId, availability }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
|
||||
? { ...entity?.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
|
||||
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
|
||||
const entity = getCheckoutEntityByShoppingCartId({ shoppingCartId, entities: s.entities });
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
|
||||
? { ...entity?.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export function domainCheckoutReducer(state, action) {
|
||||
return _domainCheckoutReducer(state, action);
|
||||
}
|
||||
|
||||
function getOrCreateCheckoutEntity({
|
||||
entities,
|
||||
processId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
processId: number;
|
||||
}): CheckoutEntity {
|
||||
let entity = entities[processId];
|
||||
|
||||
if (isNullOrUndefined(entity)) {
|
||||
return {
|
||||
processId,
|
||||
checkout: undefined,
|
||||
shoppingCart: undefined,
|
||||
shippingAddress: undefined,
|
||||
orders: [],
|
||||
payer: undefined,
|
||||
buyer: undefined,
|
||||
specialComment: '',
|
||||
notificationChannels: 0,
|
||||
olaErrorIds: [],
|
||||
customer: undefined,
|
||||
// availabilityHistory: [],
|
||||
itemAvailabilityTimestamp: {},
|
||||
};
|
||||
}
|
||||
|
||||
return { ...entity };
|
||||
}
|
||||
|
||||
function getCheckoutEntityByShoppingCartId({
|
||||
entities,
|
||||
shoppingCartId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
shoppingCartId: number;
|
||||
}): CheckoutEntity {
|
||||
return Object.values(entities).find((entity) => entity.shoppingCart?.id === shoppingCartId);
|
||||
}
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import {
|
||||
initialCheckoutState,
|
||||
storeCheckoutAdapter,
|
||||
} from './domain-checkout.state';
|
||||
|
||||
import * as DomainCheckoutActions from './domain-checkout.actions';
|
||||
import { Dictionary } from '@ngrx/entity';
|
||||
import { CheckoutEntity } from './defs/checkout.entity';
|
||||
import { isNullOrUndefined } from '@utils/common';
|
||||
|
||||
const _domainCheckoutReducer = createReducer(
|
||||
initialCheckoutState,
|
||||
on(
|
||||
DomainCheckoutActions.setShoppingCart,
|
||||
(s, { processId, shoppingCart }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
|
||||
const addedShoppingCartItems =
|
||||
shoppingCart?.items
|
||||
?.filter(
|
||||
(item) =>
|
||||
!entity.shoppingCart?.items?.find((i) => i.id === item.id),
|
||||
)
|
||||
?.map((item) => item.data) ?? [];
|
||||
|
||||
entity.shoppingCart = shoppingCart;
|
||||
|
||||
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp
|
||||
? { ...entity.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (let shoppingCartItem of addedShoppingCartItems) {
|
||||
if (shoppingCartItem.features?.orderType) {
|
||||
entity.itemAvailabilityTimestamp[
|
||||
`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`
|
||||
] = now;
|
||||
}
|
||||
}
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.setShoppingCartByShoppingCartId,
|
||||
(s, { shoppingCartId, shoppingCart }) => {
|
||||
let entity = getCheckoutEntityByShoppingCartId({
|
||||
shoppingCartId,
|
||||
entities: s.entities,
|
||||
});
|
||||
|
||||
if (!entity) {
|
||||
// No entity found for this shoppingCartId, cannot update
|
||||
return s;
|
||||
}
|
||||
|
||||
entity = { ...entity, shoppingCart };
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.checkout = checkout;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.setBuyerCommunicationDetails,
|
||||
(s, { processId, email, mobile }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
const communicationDetails = { ...entity.buyer.communicationDetails };
|
||||
communicationDetails.email = email || communicationDetails.email;
|
||||
communicationDetails.mobile = mobile || communicationDetails.mobile;
|
||||
entity.buyer = {
|
||||
...entity.buyer,
|
||||
communicationDetails,
|
||||
};
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.setNotificationChannels,
|
||||
(s, { processId, notificationChannels }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
return storeCheckoutAdapter.setOne(
|
||||
{ ...entity, notificationChannels },
|
||||
s,
|
||||
);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.setCheckoutDestination,
|
||||
(s, { processId, destination }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.checkout = {
|
||||
...entity.checkout,
|
||||
destinations: entity.checkout.destinations.map((dest) => {
|
||||
if (dest.id === destination.id) {
|
||||
return { ...dest, ...destination };
|
||||
}
|
||||
return { ...dest };
|
||||
}),
|
||||
};
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.setShippingAddress,
|
||||
(s, { processId, shippingAddress }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.shippingAddress = shippingAddress;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(DomainCheckoutActions.setBuyer, (s, { processId, buyer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.buyer = buyer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setPayer, (s, { processId, payer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.payer = payer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.setSpecialComment,
|
||||
(s, { processId, agentComment }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.specialComment = agentComment;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(DomainCheckoutActions.removeCheckoutWithProcessId, (s, { processId }) => {
|
||||
return storeCheckoutAdapter.removeOne(processId, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({
|
||||
...s,
|
||||
orders: [...s.orders, ...orders],
|
||||
})),
|
||||
on(DomainCheckoutActions.updateOrderItem, (s, { item }) => {
|
||||
const orders = [...s.orders];
|
||||
|
||||
const orderToUpdate = orders?.find((order) =>
|
||||
order.items?.find((i) => i.id === item?.id),
|
||||
);
|
||||
const orderToUpdateIndex = orders?.indexOf(orderToUpdate);
|
||||
|
||||
const orderItemToUpdate = orderToUpdate?.items?.find(
|
||||
(i) => i.id === item?.id,
|
||||
);
|
||||
const orderItemToUpdateIndex =
|
||||
orderToUpdate?.items?.indexOf(orderItemToUpdate);
|
||||
|
||||
const items = [...(orderToUpdate?.items ?? [])];
|
||||
items[orderItemToUpdateIndex] = item;
|
||||
|
||||
orders[orderToUpdateIndex] = {
|
||||
...orderToUpdate,
|
||||
items: [...items],
|
||||
};
|
||||
|
||||
return { ...s, orders: [...orders] };
|
||||
}),
|
||||
on(DomainCheckoutActions.removeAllOrders, (s) => ({
|
||||
...s,
|
||||
orders: [],
|
||||
})),
|
||||
on(DomainCheckoutActions.setOlaError, (s, { processId, olaErrorIds }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.olaErrorIds = olaErrorIds;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCustomer, (s, { processId, customer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.customer = customer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory,
|
||||
(s, { processId, shoppingCartItemId, availability }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
|
||||
? { ...entity?.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find(
|
||||
(i) => i.id === shoppingCartItemId,
|
||||
)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] =
|
||||
Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
|
||||
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
|
||||
const entity = getCheckoutEntityByShoppingCartId({
|
||||
shoppingCartId,
|
||||
entities: s.entities,
|
||||
});
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
|
||||
? { ...entity?.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find(
|
||||
(i) => i.id === shoppingCartItemId,
|
||||
)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] =
|
||||
Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export function domainCheckoutReducer(state, action) {
|
||||
return _domainCheckoutReducer(state, action);
|
||||
}
|
||||
|
||||
function getOrCreateCheckoutEntity({
|
||||
entities,
|
||||
processId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
processId: number;
|
||||
}): CheckoutEntity {
|
||||
let entity = entities[processId];
|
||||
|
||||
if (isNullOrUndefined(entity)) {
|
||||
return {
|
||||
processId,
|
||||
checkout: undefined,
|
||||
shoppingCart: undefined,
|
||||
shippingAddress: undefined,
|
||||
orders: [],
|
||||
payer: undefined,
|
||||
buyer: undefined,
|
||||
specialComment: '',
|
||||
notificationChannels: 0,
|
||||
olaErrorIds: [],
|
||||
customer: undefined,
|
||||
// availabilityHistory: [],
|
||||
itemAvailabilityTimestamp: {},
|
||||
};
|
||||
}
|
||||
|
||||
return { ...entity };
|
||||
}
|
||||
|
||||
function getCheckoutEntityByShoppingCartId({
|
||||
entities,
|
||||
shoppingCartId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
shoppingCartId: number;
|
||||
}): CheckoutEntity {
|
||||
return Object.values(entities).find(
|
||||
(entity) => entity.shoppingCart?.id === shoppingCartId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -638,6 +638,8 @@ export class CheckoutReviewComponent
|
||||
this.#checkoutService.reloadShoppingCart({
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
});
|
||||
|
||||
this.refreshAvailabilities();
|
||||
}
|
||||
|
||||
async changeAddress() {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
(click)="$event?.preventDefault(); $event?.stopPropagation()"
|
||||
>
|
||||
<shared-icon icon="shopping-cart-bold" [size]="22"></shared-icon>
|
||||
<span class="shopping-cart-count-label ml-2">{{ cartCount() }}</span>
|
||||
<span class="shopping-cart-count-label ml-2">{{ itemCount() }}</span>
|
||||
</button>
|
||||
}
|
||||
</a>
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
inject,
|
||||
computed,
|
||||
input,
|
||||
effect,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
@@ -44,6 +46,7 @@ export class ShellProcessBarItemComponent
|
||||
{
|
||||
#tabService = inject(TabService);
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
#shoppingCartResource = inject(ShoppingCartResource);
|
||||
|
||||
tab = computed(() => this.#tabService.entityMap()[this.process().id]);
|
||||
|
||||
@@ -51,6 +54,18 @@ export class ShellProcessBarItemComponent
|
||||
return this.#checkoutMetadataService.getShoppingCartId(this.process().id);
|
||||
});
|
||||
|
||||
shoppingCartIdEffect = effect(() => {
|
||||
const shoppingCartId = this.shoppingCartId();
|
||||
untracked(() =>
|
||||
this.#shoppingCartResource.setShoppingCartId(shoppingCartId),
|
||||
);
|
||||
});
|
||||
|
||||
itemCount = computed(() => {
|
||||
const shoppingCart = this.#shoppingCartResource.resource.value();
|
||||
return shoppingCart?.items?.length ?? 0;
|
||||
});
|
||||
|
||||
private _process$ = new BehaviorSubject<ApplicationProcess>(undefined);
|
||||
|
||||
process$ = this._process$.asObservable();
|
||||
@@ -61,22 +76,7 @@ export class ShellProcessBarItemComponent
|
||||
closed = new EventEmitter();
|
||||
|
||||
showCart = computed(() => {
|
||||
// TODO: Nach Prämie release kann die Logik wieder rein
|
||||
return true;
|
||||
const tab = this.tab();
|
||||
|
||||
const pdata = tab.metadata?.process_data as { count?: number };
|
||||
|
||||
if (!pdata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'count' in pdata;
|
||||
});
|
||||
|
||||
cartCount = computed(() => {
|
||||
// TODO: Use implementation from develop
|
||||
return 0;
|
||||
});
|
||||
|
||||
currentLocationUrlTree = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user