From cb7391e66f864303db393fa53324b05d5e276225 Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Mon, 10 Feb 2025 10:43:23 +0100 Subject: [PATCH 01/12] Update version numbers in azure-pipelines.yml to 4.0 --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 02904b6e7..a4a66b47f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,10 +9,10 @@ trigger: variables: # Major Version einstellen - name: 'Major' - value: '3' + value: '4' # Minor Version einstellen - name: 'Minor' - value: '4' + value: '0' - name: 'Patch' value: "$[counter(format('{0}.{1}', variables['Major'], variables['Minor']),0)]" - name: 'BuildUniqueID' From be0bff05358822382f5845f38aae0505e43545f5 Mon Sep 17 00:00:00 2001 From: Michael Auer Date: Fri, 28 Feb 2025 09:36:06 +0100 Subject: [PATCH 02/12] =?UTF-8?q?Cherry=20Pick:=20PR=201824:=20ISA-Fronten?= =?UTF-8?q?d=20-=20Expliziter=20Pfad=20f=C3=BCr=20Traefik=20IngressRoute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit c9b2762bbce567542cfe240138b798d36cb3834d) --- helmvalues/client.Feature.yaml | 4 ++-- helmvalues/client.Integration.yaml | 4 ++-- helmvalues/client.Production.yaml | 4 ++-- helmvalues/client.Staging.yaml | 4 ++-- helmvalues/client.Test.yaml | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/helmvalues/client.Feature.yaml b/helmvalues/client.Feature.yaml index 41524bf41..f412ec200 100644 --- a/helmvalues/client.Feature.yaml +++ b/helmvalues/client.Feature.yaml @@ -1,11 +1,11 @@ -fullnameOverride: isa-ui-feature +fullnameOverride: isa-ui-feature-v%MAJOR% image: harborRepo: isa/ui tag: %BUILD_BUILDNUMBER%-%BUILD_SOURCEVERSION% ingress: enabled: true - path: / + path: /isa-ui/v%MAJOR% hosts: - isa-%LOWERENVIRONMENT%.kubernetes.paragon-systems.de diff --git a/helmvalues/client.Integration.yaml b/helmvalues/client.Integration.yaml index 44eff58bf..e4ceb9e63 100644 --- a/helmvalues/client.Integration.yaml +++ b/helmvalues/client.Integration.yaml @@ -1,11 +1,11 @@ -fullnameOverride: isa-ui +fullnameOverride: isa-ui-v%MAJOR% image: harborRepo: isa/ui tag: %BUILD_BUILDNUMBER%-%BUILD_SOURCEVERSION% ingress: enabled: true - path: / + path: /isa-ui/v%MAJOR% hosts: - isa-%LOWERENVIRONMENT%.kubernetes.paragon-systems.de diff --git a/helmvalues/client.Production.yaml b/helmvalues/client.Production.yaml index 1fa5bd4c4..3cfa619f8 100644 --- a/helmvalues/client.Production.yaml +++ b/helmvalues/client.Production.yaml @@ -1,4 +1,4 @@ -fullnameOverride: isa-ui +fullnameOverride: isa-ui-v%MAJOR% image: harborRepo: isa/ui tag: %BUILD_BUILDNUMBER%-%BUILD_SOURCEVERSION% @@ -7,7 +7,7 @@ replicaCount: 2 ingress: enabled: true - path: / + path: /isa-ui/v%MAJOR% hosts: - isa.kubernetes.paragon-systems.de - isa-%LOWERENVIRONMENT%.kubernetes.paragon-systems.de diff --git a/helmvalues/client.Staging.yaml b/helmvalues/client.Staging.yaml index 41031b6a4..5fbcbbf7a 100644 --- a/helmvalues/client.Staging.yaml +++ b/helmvalues/client.Staging.yaml @@ -1,4 +1,4 @@ -fullnameOverride: isa-ui +fullnameOverride: isa-ui-v%MAJOR% image: harborRepo: isa/ui tag: %BUILD_BUILDNUMBER%-%BUILD_SOURCEVERSION% @@ -7,7 +7,7 @@ replicaCount: 2 ingress: enabled: true - path: / + path: /isa-ui/v%MAJOR% hosts: - isa-%LOWERENVIRONMENT%.kubernetes.paragon-systems.de diff --git a/helmvalues/client.Test.yaml b/helmvalues/client.Test.yaml index 6d8f28d04..38f56b3f8 100644 --- a/helmvalues/client.Test.yaml +++ b/helmvalues/client.Test.yaml @@ -1,11 +1,11 @@ -fullnameOverride: isa-ui +fullnameOverride: isa-ui-v%MAJOR% image: harborRepo: isa/ui tag: %BUILD_BUILDNUMBER%-%BUILD_SOURCEVERSION% ingress: enabled: true - path: / + path: /isa-ui/v%MAJOR% hosts: - isa-%LOWERENVIRONMENT%.kubernetes.paragon-systems.de From 32336ba5b4466bb1a80a81bfae916026562c2e69 Mon Sep 17 00:00:00 2001 From: Nino Date: Fri, 9 May 2025 12:13:25 +0200 Subject: [PATCH 03/12] Update index file return data-access --- libs/oms/data-access/src/lib/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/oms/data-access/src/lib/index.ts b/libs/oms/data-access/src/lib/index.ts index 2437e6200..db47ebe87 100644 --- a/libs/oms/data-access/src/lib/index.ts +++ b/libs/oms/data-access/src/lib/index.ts @@ -1,12 +1,13 @@ export * from './errors'; export * from './models'; +export * from './questions'; export * from './return-details.service'; export * from './return-details.store'; +export * from './return-print-receipts.service'; export * from './return-process.service'; export * from './return-process.store'; export * from './return-search.service'; export * from './return-search.store'; -export * from './return-print-receipts.service'; export * from './return-task-list.service'; export * from './return-task-list.store'; export * from './schemas'; From 6e7c56fcb95f9aaf0271842e2948acedf8ae9a44 Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Wed, 28 May 2025 21:32:41 +0200 Subject: [PATCH 04/12] style(errors): standardize quotation marks in error exports --- .../src/lib/errors/return-process/index.ts | 4 +- libs/oms/data-access/src/lib/models/index.ts | 42 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/libs/oms/data-access/src/lib/errors/return-process/index.ts b/libs/oms/data-access/src/lib/errors/return-process/index.ts index d5f9bdceb..4b8824343 100644 --- a/libs/oms/data-access/src/lib/errors/return-process/index.ts +++ b/libs/oms/data-access/src/lib/errors/return-process/index.ts @@ -1,2 +1,2 @@ -export * from './create-return-process.error'; -export * from './return-process-is-not-complete.error'; +export * from "./create-return-process.error"; +export * from "./return-process-is-not-complete.error"; diff --git a/libs/oms/data-access/src/lib/models/index.ts b/libs/oms/data-access/src/lib/models/index.ts index 6bd66f572..90035cd7a 100644 --- a/libs/oms/data-access/src/lib/models/index.ts +++ b/libs/oms/data-access/src/lib/models/index.ts @@ -1,21 +1,21 @@ -export * from './address-type'; -export * from './buyer'; -export * from './can-return'; -export * from './eligible-for-return'; -export * from './gender'; -export * from './product'; -export * from './quantity'; -export * from './receipt-item-task-list-item'; -export * from './receipt-item'; -export * from './receipt-list-item'; -export * from './receipt-type'; -export * from './receipt'; -export * from './return-info'; -export * from './return-process-answer'; -export * from './return-process-question-key'; -export * from './return-process-question-type'; -export * from './return-process-question'; -export * from './return-process-status'; -export * from './return-process'; -export * from './shipping-type'; -export * from './task-action-type'; +export * from "./address-type"; +export * from "./buyer"; +export * from "./can-return"; +export * from "./eligible-for-return"; +export * from "./gender"; +export * from "./product"; +export * from "./quantity"; +export * from "./receipt-item-task-list-item"; +export * from "./receipt-item"; +export * from "./receipt-list-item"; +export * from "./receipt-type"; +export * from "./receipt"; +export * from "./return-info"; +export * from "./return-process-answer"; +export * from "./return-process-question-key"; +export * from "./return-process-question-type"; +export * from "./return-process-question"; +export * from "./return-process-status"; +export * from "./return-process"; +export * from "./shipping-type"; +export * from "./task-action-type"; From 9a4121e2bf3810e3301f41f3fa8eeeffea1376b9 Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Mon, 16 Jun 2025 10:53:58 +0200 Subject: [PATCH 05/12] fix(return-details): correct storage key retrieval in ReturnDetailsStore --- libs/oms/data-access/src/lib/stores/return-details.store.ts | 2 +- .../feature/return-details/src/lib/return-details.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/oms/data-access/src/lib/stores/return-details.store.ts b/libs/oms/data-access/src/lib/stores/return-details.store.ts index 4b7bd43ce..709d8024c 100644 --- a/libs/oms/data-access/src/lib/stores/return-details.store.ts +++ b/libs/oms/data-access/src/lib/stores/return-details.store.ts @@ -58,7 +58,7 @@ export const ReturnDetailsStore = signalStore( _storage: inject(SessionStorageProvider), })), withMethods((store) => ({ - _storageKey: () => `ReturnDetailsStore:${store._storageId}`, + _storageKey: () => `ReturnDetailsStore:${store._storageId()}`, })), withMethods((store) => ({ _storeState: () => { diff --git a/libs/oms/feature/return-details/src/lib/return-details.component.ts b/libs/oms/feature/return-details/src/lib/return-details.component.ts index fe5c2dc13..1ae500c1c 100644 --- a/libs/oms/feature/return-details/src/lib/return-details.component.ts +++ b/libs/oms/feature/return-details/src/lib/return-details.component.ts @@ -44,7 +44,7 @@ import { groupBy } from 'lodash'; }) export class ReturnDetailsComponent { #logger = logger(() => ({ - component: ReturnDetailsComponent.name, + component: 'ReturnDetailsComponent', itemId: this.receiptId(), processId: this.processId(), params: this.params(), From e9affd2359e9db2b27d4e2cf0ac6e698e253dd67 Mon Sep 17 00:00:00 2001 From: Nino Date: Tue, 17 Jun 2025 16:52:03 +0200 Subject: [PATCH 06/12] fix(return-details): Small Layout Fix, Refs: #5171 --- .../return-details-order-group-item-controls.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html index 0c3a45f44..9f24a0441 100644 --- a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html +++ b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html @@ -1,4 +1,4 @@ -
+
@if (quantityDropdownValues().length > 1) { Date: Mon, 23 Jun 2025 15:24:26 +0000 Subject: [PATCH 07/12] Merged PR 1868: fix(oms-return-search): resolve issues in return search result item rendering fix(oms-return-search): resolve issues in return search result item rendering Corrects rendering logic and improves template structure for the return search result item component. Ensures compliance with Angular control flow best practices and enhances maintainability. Ref: #5190 --- .../ui/item-rows/client-row.stories.ts | 24 +++++++++++-------- .../return-search-result-item.component.html | 9 ++++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/apps/isa-app/stories/ui/item-rows/client-row.stories.ts b/apps/isa-app/stories/ui/item-rows/client-row.stories.ts index 506ca995b..eecd53083 100644 --- a/apps/isa-app/stories/ui/item-rows/client-row.stories.ts +++ b/apps/isa-app/stories/ui/item-rows/client-row.stories.ts @@ -1,9 +1,13 @@ -import { type Meta, type StoryObj, moduleMetadata } from '@storybook/angular'; -import { ClientRowComponent, ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows'; +import { type Meta, type StoryObj, moduleMetadata } from "@storybook/angular"; +import { + ClientRowComponent, + ClientRowImports, + ItemRowDataImports, +} from "@isa/ui/item-rows"; const meta: Meta = { component: ClientRowComponent, - title: 'ui/item-rows/ClientRow', + title: "ui/item-rows/ClientRow", decorators: [ moduleMetadata({ imports: [ClientRowImports, ItemRowDataImports], @@ -21,25 +25,25 @@ const meta: Meta = { Belegdatum - + 01.11.2024 - Rechnugsnr. + Beleg-Nr. - + 1234567890 Vorgangs-ID - - - 640175214390060/0 - + + + 640175214390060/0 + diff --git a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.html b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.html index 2524c6c92..f79b84025 100644 --- a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.html +++ b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.html @@ -1,4 +1,7 @@ - +

{{ name() }}

@@ -7,12 +10,12 @@ Belegdatum - {{ receiptDate() | date: 'dd.MM.yy' }} + {{ receiptDate() | date: "dd.MM.yy" }} - Rechnugsnr. + Beleg-Nr. {{ receiptNumber() }} From 1b26a44a37ad6dda75b4c159ec59065703788382 Mon Sep 17 00:00:00 2001 From: Nino Righi Date: Mon, 23 Jun 2025 15:25:34 +0000 Subject: [PATCH 08/12] Merged PR 1869: fix(oms-task-list-item): address styling and layout issues in return task lis... fix(oms-task-list-item): address styling and layout issues in return task list item Improves SCSS for the return task list item component to ensure consistent appearance and resolve layout inconsistencies. Enhances maintainability and visual alignment with design standards. Ref: #5191 --- .../return-task-list-item.component.scss | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/libs/oms/shared/task-list/src/lib/return-task-list/return-task-list-item/return-task-list-item.component.scss b/libs/oms/shared/task-list/src/lib/return-task-list/return-task-list-item/return-task-list-item.component.scss index c4f04bf44..0cd6b06d6 100644 --- a/libs/oms/shared/task-list/src/lib/return-task-list/return-task-list-item/return-task-list-item.component.scss +++ b/libs/oms/shared/task-list/src/lib/return-task-list/return-task-list-item/return-task-list-item.component.scss @@ -3,12 +3,35 @@ } .oms-shared-return-task-list-item__review { - @apply grid grid-cols-[1fr,1fr] desktop:grid-cols-[1fr,1fr,minmax(20rem,auto)] gap-x-6 py-6 text-isa-secondary-900 items-center border-b border-solid border-isa-neutral-300 last:pb-0 last:border-none; + @apply grid grid-cols-[1fr,1fr] desktop:grid-cols-[1fr,1fr,minmax(20rem,auto)] gap-x-6 desktop:gap-y-6 py-6 text-isa-secondary-900 items-center border-b border-solid border-isa-neutral-300 last:pb-0 last:border-none; + + &:has(.task-unknown-actions):has(.tolino-print-cta) { + .tolino-print-cta { + @apply desktop:justify-self-start; + } + } @media screen and (max-width: 1024px) { grid-template-areas: - 'product infos' - 'unknown-comment actions'; + "product infos" + "unknown-comment actions"; + + .tolino-print-cta, + .task-unknown-actions { + @apply mt-6 desktop:mt-0; + grid-area: actions; + } + + &:has(.task-unknown-actions):has(.tolino-print-cta) { + .tolino-print-cta { + @apply mt-0 self-start; + grid-area: print; + } + + grid-template-areas: + "product print" + "unknown-comment actions"; + } .product-info { grid-area: product; @@ -18,12 +41,6 @@ grid-area: infos; } - .tolino-print-cta, - .task-unknown-actions { - @apply mt-6 desktop:mt-0; - grid-area: actions; - } - .processing-comment-unknown { @apply mt-6 desktop:mt-0; grid-area: unknown-comment; @@ -48,7 +65,7 @@ } .task-unknown-actions { - @apply flex flex-row gap-3 h-full py-2 items-center; + @apply flex flex-row gap-3 h-full py-2 items-center justify-self-end; } .processing-comment { From f051a97e539866aa8e000a2f72dff8c6ec364a80 Mon Sep 17 00:00:00 2001 From: Nino Righi Date: Mon, 23 Jun 2025 15:32:56 +0000 Subject: [PATCH 09/12] Merged PR 1871: fix(ui-dropdown): improve dropdown usability and conditional rendering fix(ui-dropdown): improve dropdown usability and conditional rendering Refines the logic for displaying quantity and product category dropdowns in the return details order group item controls. Ensures dropdowns are only shown when appropriate and maintains accessibility and user experience. Ref: #5189 --- ...rn-details-order-group-item-controls.component.html | 4 +++- ...rn-details-order-group-item-controls.component.scss | 10 +++++++--- libs/ui/input-controls/src/lib/dropdown/_dropdown.scss | 6 +++++- .../src/lib/dropdown/dropdown.component.html | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html index 9f24a0441..f0308112c 100644 --- a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html +++ b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html @@ -1,4 +1,6 @@ -
+
@if (quantityDropdownValues().length > 1) { {{ viewLabel() }} +{{ viewLabel() }} Date: Mon, 23 Jun 2025 21:23:27 +0000 Subject: [PATCH 10/12] Merged PR 1870: fix(oms-return-search): fix display and logic issues in return search results fix(oms-return-search): fix display and logic issues in return search results Resolve display inconsistencies and correct logic in the return search result component to improve user experience and maintain alignment with design and business requirements. Ref: #5009 --- .../return-search-main.component.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.ts b/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.ts index 42c92a296..f06017369 100644 --- a/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.ts +++ b/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.ts @@ -3,28 +3,28 @@ import { Component, computed, inject, -} from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { CallbackResult, ListResponseArgs } from '@isa/common/data-access'; -import { injectActivatedProcessId } from '@isa/core/process'; +} from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { CallbackResult, ListResponseArgs } from "@isa/common/data-access"; +import { injectActivatedProcessId } from "@isa/core/process"; import { ReceiptListItem, ReturnSearchStatus, ReturnSearchStore, -} from '@isa/oms/data-access'; -import { ReturnTaskListComponent } from '@isa/oms/shared/task-list'; +} from "@isa/oms/data-access"; +import { ReturnTaskListComponent } from "@isa/oms/shared/task-list"; import { FilterService, SearchBarInputComponent, FilterInputMenuButtonComponent, -} from '@isa/shared/filter'; -import { IconButtonComponent } from '@isa/ui/buttons'; -import { TooltipIconComponent } from '@isa/ui/tooltip'; +} from "@isa/shared/filter"; +import { IconButtonComponent } from "@isa/ui/buttons"; +import { TooltipIconComponent } from "@isa/ui/tooltip"; @Component({ - selector: 'oms-feature-return-search-main', - templateUrl: './return-search-main.component.html', - styleUrls: ['./return-search-main.component.scss'], + selector: "oms-feature-return-search-main", + templateUrl: "./return-search-main.component.html", + styleUrls: ["./return-search-main.component.scss"], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ SearchBarInputComponent, @@ -55,7 +55,7 @@ export class ReturnSearchMainComponent { }); filterInputs = computed(() => - this._filterService.inputs().filter((input) => input.group === 'filter'), + this._filterService.inputs().filter((input) => input.group === "filter"), ); // TODO: Suche als Provider in FilterService auslagern (+ Cancel Search, + Fetching Status) @@ -77,10 +77,10 @@ export class ReturnSearchMainComponent { }: CallbackResult>) => { if (data) { if (data.result.length === 1) { - this.navigate(['receipt', data.result[0].id]); - } else if (data.result.length > 1) { - this.navigate(['receipts']); + return this.navigate(["receipt", data.result[0].id]); } + + return this.navigate(["receipts"]); } }; From 6fee35c7566b783daa758b016b6807cd1ca20a99 Mon Sep 17 00:00:00 2001 From: Nino Righi Date: Wed, 25 Jun 2025 08:35:43 +0000 Subject: [PATCH 11/12] Merged PR 1872: fix(isa-app-moment-locale): correct locale initialization for date formatting fix(isa-app-moment-locale): correct locale initialization for date formatting Ensures proper setup of moment.js locale in the ISA app to provide accurate date and time formatting for users. Addresses issues with incorrect or inconsistent locale application. Ref: #5188 --- apps/isa-app/src/main.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/isa-app/src/main.ts b/apps/isa-app/src/main.ts index 8ef65e650..ff648a905 100644 --- a/apps/isa-app/src/main.ts +++ b/apps/isa-app/src/main.ts @@ -1,21 +1,22 @@ -import { enableProdMode, isDevMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { CONFIG_DATA } from '@isa/core/config'; -import { setDefaultOptions } from 'date-fns'; -import { de } from 'date-fns/locale'; -import * as moment from 'moment'; +import { enableProdMode, isDevMode } from "@angular/core"; +import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; +import { CONFIG_DATA } from "@isa/core/config"; +import { setDefaultOptions } from "date-fns"; +import { de } from "date-fns/locale"; +import * as moment from "moment"; +import "moment/locale/de"; setDefaultOptions({ locale: de }); -moment.locale('de'); +moment.locale("de"); -import { AppModule } from './app/app.module'; +import { AppModule } from "./app/app.module"; if (!isDevMode()) { enableProdMode(); } async function bootstrap() { - const configRes = await fetch('/config/config.json'); + const configRes = await fetch("/config/config.json"); const config = await configRes.json(); From 7c907645dc69e7ff890d5ba3a8daf1e1d75f75b0 Mon Sep 17 00:00:00 2001 From: Nino Righi Date: Thu, 10 Jul 2025 11:32:42 +0000 Subject: [PATCH 12/12] Merged PR 1880: hotfix(oms-data-access): initial implementation of OMS data access layer hotfix(oms-data-access): initial implementation of OMS data access layer Introduce the foundational OMS data access module, including service scaffolding and integration points for future API communication. This establishes a clear separation of concerns for order management system data retrieval and manipulation, following project architecture guidelines. Ref: #5210 --- .../data-access/src/lib/models/index.ts | 4 +- .../create-return-process.error.test.ts | 124 +++++++++++-- .../create-return-process.error.ts | 26 ++- ...turn-receipt-values-mapping.helper.spec.ts | 127 +++++++------ .../return-receipt-values-mapping.helper.ts | 20 +- libs/oms/data-access/src/lib/models/index.ts | 46 ++--- .../src/lib/models/return-process.ts | 7 +- .../lib/services/return-details.service.ts | 38 ++-- .../src/lib/stores/return-details.store.ts | 160 +++++++--------- .../lib/stores/return-process.store.spec.ts | 170 ++++++++++------- .../src/lib/stores/return-process.store.ts | 40 ++-- ...s-order-group-item-controls.component.html | 2 +- ...rder-group-item-controls.component.spec.ts | 174 +++++++++++------- ...ils-order-group-item-controls.component.ts | 62 ++----- ...rn-details-order-group-item.component.html | 8 +- ...turn-details-order-group-item.component.ts | 65 +++---- .../src/lib/return-details.component.ts | 72 ++++---- 17 files changed, 666 insertions(+), 479 deletions(-) diff --git a/libs/catalogue/data-access/src/lib/models/index.ts b/libs/catalogue/data-access/src/lib/models/index.ts index 3d3352801..126444898 100644 --- a/libs/catalogue/data-access/src/lib/models/index.ts +++ b/libs/catalogue/data-access/src/lib/models/index.ts @@ -1,2 +1,2 @@ -export * from './item'; -export * from './product'; +export * from "./item"; +export * from "./product"; diff --git a/libs/oms/data-access/src/lib/errors/return-process/create-return-process.error.test.ts b/libs/oms/data-access/src/lib/errors/return-process/create-return-process.error.test.ts index 4d3cdab72..d2437e251 100644 --- a/libs/oms/data-access/src/lib/errors/return-process/create-return-process.error.test.ts +++ b/libs/oms/data-access/src/lib/errors/return-process/create-return-process.error.test.ts @@ -1,27 +1,51 @@ -import { DataAccessError } from '@isa/common/data-access'; -import { Receipt, ReceiptItem } from '../../models'; +import { DataAccessError } from "@isa/common/data-access"; +import { Receipt, ReceiptItem } from "../../models"; import { CreateReturnProcessError, CreateReturnProcessErrorReason, CreateReturnProcessErrorMessages, -} from './create-return-process.error'; +} from "./create-return-process.error"; +import { ProductCategory } from "../../questions"; -describe('CreateReturnProcessError', () => { +describe("CreateReturnProcessError", () => { const params = { processId: 123, returns: [ { receipt: { id: 321 } as Receipt, - items: [] as ReceiptItem[], + items: [ + // Provide at least one valid item object, or an empty array if testing "no items" + // For NO_RETURNABLE_ITEMS, an empty array is valid, but must match the expected shape + // So, keep as [], but type is now correct + ], }, ], }; - it('should create an error instance with NO_RETURNABLE_ITEMS reason', () => { + // For tests that require items, use the correct shape: + const validParams = { + processId: 123, + returns: [ + { + receipt: { id: 321 } as Receipt, + items: [ + { + receiptItem: { id: 111 } as ReceiptItem, + quantity: 1, + category: "A" as ProductCategory, + }, + ], + }, + ], + }; + + it("should create an error instance with NO_RETURNABLE_ITEMS reason", () => { + // Arrange, Act const error = new CreateReturnProcessError( CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS, params, ); + // Assert expect(error).toBeInstanceOf(CreateReturnProcessError); expect(error).toBeInstanceOf(DataAccessError); expect(error.reason).toBe( @@ -33,25 +57,103 @@ describe('CreateReturnProcessError', () => { CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS ], ); - expect(error.code).toBe('CREATE_RETURN_PROCESS'); + expect(error.code).toBe("CREATE_RETURN_PROCESS"); }); - it('should create an error instance with MISMATCH_RETURNABLE_ITEMS reason', () => { + it("should create an error instance with MISMATCH_RETURNABLE_ITEMS reason", () => { + // Arrange, Act const error = new CreateReturnProcessError( CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS, - params, + validParams, ); + // Assert expect(error).toBeInstanceOf(CreateReturnProcessError); expect(error).toBeInstanceOf(DataAccessError); expect(error.reason).toBe( CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS, ); - expect(error.params).toEqual(params); + expect(error.params).toEqual(validParams); expect(error.message).toBe( CreateReturnProcessErrorMessages[ CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS ], ); - expect(error.code).toBe('CREATE_RETURN_PROCESS'); + expect(error.code).toBe("CREATE_RETURN_PROCESS"); + }); + + it("should expose the correct params structure", () => { + const error = new CreateReturnProcessError( + CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS, + params, + ); + expect(error.params).toHaveProperty("processId", 123); + expect(error.params).toHaveProperty("returns"); + expect(Array.isArray(error.params.returns)).toBe(true); + expect(error.params.returns[0]).toHaveProperty("receipt"); + expect(error.params.returns[0]).toHaveProperty("items"); + }); + + it("should throw and be catchable as CreateReturnProcessError", () => { + try { + throw new CreateReturnProcessError( + CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS, + params, + ); + } catch (err) { + expect(err).toBeInstanceOf(CreateReturnProcessError); + expect((err as CreateReturnProcessError).reason).toBe( + CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS, + ); + } + }); + + it("should use the correct message for each reason", () => { + Object.values(CreateReturnProcessErrorReason).forEach((reason) => { + const error = new CreateReturnProcessError(reason, params); + expect(error.message).toBe(CreateReturnProcessErrorMessages[reason]); + }); + }); + + it('should have code "CREATE_RETURN_PROCESS" for all reasons', () => { + Object.values(CreateReturnProcessErrorReason).forEach((reason) => { + const error = new CreateReturnProcessError(reason, params); + expect(error.code).toBe("CREATE_RETURN_PROCESS"); + }); + }); + + it("should support params with multiple returns and items", () => { + const extendedParams = { + processId: 999, + returns: [ + { + receipt: { id: 1 } as Receipt, + items: [ + { + receiptItem: { id: 10 } as ReceiptItem, + quantity: 2, + category: "A" as ProductCategory, + }, + ], + }, + { + receipt: { id: 2 } as Receipt, + items: [ + { + receiptItem: { id: 20 } as ReceiptItem, + quantity: 1, + category: "B" as ProductCategory, + }, + ], + }, + ], + }; + const error = new CreateReturnProcessError( + CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS, + extendedParams, + ); + expect(error.params.processId).toBe(999); + expect(error.params.returns.length).toBe(2); + expect(error.params.returns[0].items[0].quantity).toBe(2); + expect(error.params.returns[1].items[0].category).toBe("B"); }); }); diff --git a/libs/oms/data-access/src/lib/errors/return-process/create-return-process.error.ts b/libs/oms/data-access/src/lib/errors/return-process/create-return-process.error.ts index 54f4414da..2847845fc 100644 --- a/libs/oms/data-access/src/lib/errors/return-process/create-return-process.error.ts +++ b/libs/oms/data-access/src/lib/errors/return-process/create-return-process.error.ts @@ -1,13 +1,14 @@ -import { DataAccessError } from '@isa/common/data-access'; -import { Receipt, ReceiptItem } from '../../models'; +import { DataAccessError } from "@isa/common/data-access"; +import { Receipt, ReceiptItem } from "../../models"; +import { ProductCategory } from "../../questions"; /** * Enum-like object defining possible reasons for return process creation failures. * Used to provide consistent and type-safe error categorization. */ export const CreateReturnProcessErrorReason = { - NO_RETURNABLE_ITEMS: 'NO_RETURNABLE_ITEMS', - MISMATCH_RETURNABLE_ITEMS: 'MISMATCH_RETURNABLE_ITEMS', + NO_RETURNABLE_ITEMS: "NO_RETURNABLE_ITEMS", + MISMATCH_RETURNABLE_ITEMS: "MISMATCH_RETURNABLE_ITEMS", } as const; /** @@ -32,9 +33,9 @@ export const CreateReturnProcessErrorMessages: Record< string > = { [CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS]: - 'No returnable items found.', + "No returnable items found.", [CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS]: - 'Mismatch in the number of returnable items.', + "Mismatch in the number of returnable items.", }; /** @@ -73,14 +74,21 @@ export const CreateReturnProcessErrorMessages: Record< * } * ``` */ -export class CreateReturnProcessError extends DataAccessError<'CREATE_RETURN_PROCESS'> { +export class CreateReturnProcessError extends DataAccessError<"CREATE_RETURN_PROCESS"> { constructor( public readonly reason: CreateReturnProcessErrorReason, public readonly params: { processId: number; - returns: { receipt: Receipt; items: ReceiptItem[] }[]; + returns: { + receipt: Receipt; + items: { + receiptItem: ReceiptItem; + quantity: number; + category: ProductCategory; + }[]; + }[]; }, ) { - super('CREATE_RETURN_PROCESS', CreateReturnProcessErrorMessages[reason]); + super("CREATE_RETURN_PROCESS", CreateReturnProcessErrorMessages[reason]); } } diff --git a/libs/oms/data-access/src/lib/helpers/return-process/return-receipt-values-mapping.helper.spec.ts b/libs/oms/data-access/src/lib/helpers/return-process/return-receipt-values-mapping.helper.spec.ts index a5ec5b885..40b21b717 100644 --- a/libs/oms/data-access/src/lib/helpers/return-process/return-receipt-values-mapping.helper.spec.ts +++ b/libs/oms/data-access/src/lib/helpers/return-process/return-receipt-values-mapping.helper.spec.ts @@ -1,39 +1,39 @@ -import { returnReceiptValuesMapping } from './return-receipt-values-mapping.helper'; -import { PropertyNullOrUndefinedError } from '@isa/common/data-access'; -import { getReturnProcessQuestions } from './get-return-process-questions.helper'; -import { getReturnInfo } from './get-return-info.helper'; -import { serializeReturnDetails } from './return-details-mapping.helper'; +import { returnReceiptValuesMapping } from "./return-receipt-values-mapping.helper"; +import { PropertyNullOrUndefinedError } from "@isa/common/data-access"; +import { getReturnProcessQuestions } from "./get-return-process-questions.helper"; +import { getReturnInfo } from "./get-return-info.helper"; +import { serializeReturnDetails } from "./return-details-mapping.helper"; // Mock dependencies -jest.mock('./get-return-process-questions.helper', () => ({ +jest.mock("./get-return-process-questions.helper", () => ({ getReturnProcessQuestions: jest.fn(), })); -jest.mock('./get-return-info.helper', () => ({ +jest.mock("./get-return-info.helper", () => ({ getReturnInfo: jest.fn(), })); -jest.mock('./return-details-mapping.helper', () => ({ +jest.mock("./return-details-mapping.helper", () => ({ serializeReturnDetails: jest.fn(), })); -describe('returnReceiptValuesMapping', () => { +describe("returnReceiptValuesMapping", () => { const processMock: any = { receiptItem: { - id: 'item-1', - quantity: { quantity: 2 }, - features: { category: 'shoes' }, + id: "item-1", }, - answers: { foo: 'bar' }, + quantity: 2, // <-- Add this + productCategory: "shoes", // <-- Add this + answers: { foo: "bar" }, }; - const questionsMock = [{ id: 'q1' }]; + const questionsMock = [{ id: "q1" }]; const returnInfoMock = { - comment: 'Test comment', - itemCondition: 'NEW', - otherProduct: 'Other', - returnDetails: { detail: 'details' }, - returnReason: 'Damaged', + comment: "Test comment", + itemCondition: "NEW", + otherProduct: "Other", + returnDetails: { detail: "details" }, + returnReason: "Damaged", }; - const serializedDetails = { detail: 'serialized' }; + const serializedDetails = { detail: "serialized" }; beforeEach(() => { jest.clearAllMocks(); @@ -42,32 +42,24 @@ describe('returnReceiptValuesMapping', () => { (serializeReturnDetails as jest.Mock).mockReturnValue(serializedDetails); }); - it('should map values correctly when all dependencies return valid data', () => { + it("should map values correctly when all dependencies return valid data", () => { // Act const result = returnReceiptValuesMapping(processMock); // Assert expect(result).toEqual({ quantity: 2, - comment: 'Test comment', - itemCondition: 'NEW', - otherProduct: 'Other', + comment: "Test comment", + itemCondition: "NEW", + otherProduct: "Other", returnDetails: serializedDetails, - returnReason: 'Damaged', - category: 'shoes', - receiptItem: { id: 'item-1' }, + returnReason: "Damaged", + category: "shoes", + receiptItem: { id: "item-1" }, }); - expect(getReturnProcessQuestions).toHaveBeenCalledWith(processMock); - expect(getReturnInfo).toHaveBeenCalledWith({ - questions: questionsMock, - answers: processMock.answers, - }); - expect(serializeReturnDetails).toHaveBeenCalledWith( - returnInfoMock.returnDetails, - ); }); - it('should throw PropertyNullOrUndefinedError if questions is undefined', () => { + it("should throw PropertyNullOrUndefinedError if questions is undefined", () => { // Arrange (getReturnProcessQuestions as jest.Mock).mockReturnValue(undefined); @@ -75,10 +67,10 @@ describe('returnReceiptValuesMapping', () => { expect(() => returnReceiptValuesMapping(processMock)).toThrow( PropertyNullOrUndefinedError, ); - expect(() => returnReceiptValuesMapping(processMock)).toThrow('questions'); + expect(() => returnReceiptValuesMapping(processMock)).toThrow("questions"); }); - it('should throw PropertyNullOrUndefinedError if returnInfo is undefined', () => { + it("should throw PropertyNullOrUndefinedError if returnInfo is undefined", () => { // Arrange (getReturnInfo as jest.Mock).mockReturnValue(undefined); @@ -86,28 +78,55 @@ describe('returnReceiptValuesMapping', () => { expect(() => returnReceiptValuesMapping(processMock)).toThrow( PropertyNullOrUndefinedError, ); - expect(() => returnReceiptValuesMapping(processMock)).toThrow('returnInfo'); + expect(() => returnReceiptValuesMapping(processMock)).toThrow("returnInfo"); }); - it('should handle missing category gracefully', () => { - // Arrange - const processNoCategory = { - ...processMock, - receiptItem: { ...processMock.receiptItem, features: {} }, - }; - - // Act - const result = returnReceiptValuesMapping(processNoCategory); - - // Assert - expect(result?.category).toBeUndefined(); - }); - - it('should handle missing receiptItem gracefully (may throw)', () => { + it("should handle missing receiptItem gracefully (may throw)", () => { // Arrange const processNoReceiptItem = { ...processMock, receiptItem: undefined }; // Act & Assert expect(() => returnReceiptValuesMapping(processNoReceiptItem)).toThrow(); }); + + // Additional tests for edge cases and error scenarios + + it("should return correct quantity when process.quantity is 0", () => { + const processZeroQuantity = { ...processMock, quantity: 0 }; + const result = returnReceiptValuesMapping(processZeroQuantity); + expect(result?.quantity).toBe(0); + }); + + it("should propagate the correct receiptItem id", () => { + const result = returnReceiptValuesMapping(processMock); + expect(result?.receiptItem).toEqual({ id: "item-1" }); + }); + + it("should throw if process is null", () => { + expect(() => returnReceiptValuesMapping(null as any)).toThrow(); + }); + + it("should throw if process is undefined", () => { + expect(() => returnReceiptValuesMapping(undefined as any)).toThrow(); + }); + + it("should call serializeReturnDetails with undefined if returnDetails is missing", () => { + // Arrange + const returnInfoNoDetails = { ...returnInfoMock, returnDetails: undefined }; + (getReturnInfo as jest.Mock).mockReturnValue(returnInfoNoDetails); + + // Act + returnReceiptValuesMapping(processMock); + + // Assert + expect(serializeReturnDetails).toHaveBeenCalledWith(undefined); + }); + + it("should return undefined if process.quantity is undefined", () => { + const processNoQuantity = { ...processMock }; + delete processNoQuantity.quantity; + // Should not throw, but quantity will be undefined in result + const result = returnReceiptValuesMapping(processNoQuantity); + expect(result?.quantity).toBeUndefined(); + }); }); diff --git a/libs/oms/data-access/src/lib/helpers/return-process/return-receipt-values-mapping.helper.ts b/libs/oms/data-access/src/lib/helpers/return-process/return-receipt-values-mapping.helper.ts index 0e43bcc2d..e40f1a3ae 100644 --- a/libs/oms/data-access/src/lib/helpers/return-process/return-receipt-values-mapping.helper.ts +++ b/libs/oms/data-access/src/lib/helpers/return-process/return-receipt-values-mapping.helper.ts @@ -1,16 +1,16 @@ -import { ReturnProcess } from '../../models'; -import { ReturnReceiptValues } from '../../schemas'; -import { getReturnProcessQuestions } from './get-return-process-questions.helper'; -import { getReturnInfo } from './get-return-info.helper'; -import { PropertyNullOrUndefinedError } from '@isa/common/data-access'; -import { serializeReturnDetails } from './return-details-mapping.helper'; +import { ReturnProcess } from "../../models"; +import { ReturnReceiptValues } from "../../schemas"; +import { getReturnProcessQuestions } from "./get-return-process-questions.helper"; +import { getReturnInfo } from "./get-return-info.helper"; +import { PropertyNullOrUndefinedError } from "@isa/common/data-access"; +import { serializeReturnDetails } from "./return-details-mapping.helper"; export const returnReceiptValuesMapping = ( process: ReturnProcess, ): ReturnReceiptValues | undefined => { const questions = getReturnProcessQuestions(process); if (!questions) { - throw new PropertyNullOrUndefinedError('questions'); + throw new PropertyNullOrUndefinedError("questions"); } const returnInfo = getReturnInfo({ @@ -19,17 +19,17 @@ export const returnReceiptValuesMapping = ( }); if (!returnInfo) { - throw new PropertyNullOrUndefinedError('returnInfo'); + throw new PropertyNullOrUndefinedError("returnInfo"); } return { - quantity: process.receiptItem.quantity.quantity, + quantity: process.quantity, comment: returnInfo.comment, itemCondition: returnInfo.itemCondition, otherProduct: returnInfo.otherProduct, returnDetails: serializeReturnDetails(returnInfo.returnDetails), returnReason: returnInfo.returnReason, - category: process?.receiptItem?.features?.['category'], + category: process.productCategory, receiptItem: { id: process.receiptItem.id, }, diff --git a/libs/oms/data-access/src/lib/models/index.ts b/libs/oms/data-access/src/lib/models/index.ts index 0e6d3d6e4..3a2ceb18c 100644 --- a/libs/oms/data-access/src/lib/models/index.ts +++ b/libs/oms/data-access/src/lib/models/index.ts @@ -1,23 +1,23 @@ -export * from './address-type'; -export * from './buyer'; -export * from './can-return'; -export * from './eligible-for-return'; -export * from './gender'; -export * from './product'; -export * from './quantity'; -export * from './receipt-item-list-item'; -export * from './receipt-item-task-list-item'; -export * from './receipt-item'; -export * from './receipt-list-item'; -export * from './receipt-type'; -export * from './receipt'; -export * from './return-info'; -export * from './return-process-answer'; -export * from './return-process-question-key'; -export * from './return-process-question-type'; -export * from './return-process-question'; -export * from './return-process-status'; -export * from './return-process'; -export * from './shipping-address-2'; -export * from './shipping-type'; -export * from './task-action-type'; +export * from "./address-type"; +export * from "./buyer"; +export * from "./can-return"; +export * from "./eligible-for-return"; +export * from "./gender"; +export * from "./product"; +export * from "./quantity"; +export * from "./receipt-item-list-item"; +export * from "./receipt-item-task-list-item"; +export * from "./receipt-item"; +export * from "./receipt-list-item"; +export * from "./receipt-type"; +export * from "./receipt"; +export * from "./return-info"; +export * from "./return-process-answer"; +export * from "./return-process-question-key"; +export * from "./return-process-question-type"; +export * from "./return-process-question"; +export * from "./return-process-status"; +export * from "./return-process"; +export * from "./shipping-address-2"; +export * from "./shipping-type"; +export * from "./task-action-type"; diff --git a/libs/oms/data-access/src/lib/models/return-process.ts b/libs/oms/data-access/src/lib/models/return-process.ts index d100c4625..9c0e8b570 100644 --- a/libs/oms/data-access/src/lib/models/return-process.ts +++ b/libs/oms/data-access/src/lib/models/return-process.ts @@ -1,5 +1,5 @@ -import { Receipt } from './receipt'; -import { ReceiptItem } from './receipt-item'; +import { Receipt } from "./receipt"; +import { ReceiptItem } from "./receipt-item"; /** * Interface representing a return process within the OMS system. @@ -21,6 +21,7 @@ export interface ReturnProcess { receiptItem: ReceiptItem; receiptDate: string | undefined; answers: Record; - productCategory?: string; + productCategory: string; + quantity: number; returnReceipt?: Receipt; } diff --git a/libs/oms/data-access/src/lib/services/return-details.service.ts b/libs/oms/data-access/src/lib/services/return-details.service.ts index 6965592ea..161566ccf 100644 --- a/libs/oms/data-access/src/lib/services/return-details.service.ts +++ b/libs/oms/data-access/src/lib/services/return-details.service.ts @@ -1,17 +1,17 @@ -import { inject, Injectable } from '@angular/core'; +import { inject, Injectable } from "@angular/core"; import { FetchReturnDetails, FetchReturnDetailsSchema, ReturnReceiptValues, -} from '../schemas'; -import { firstValueFrom } from 'rxjs'; -import { ReceiptService } from '@generated/swagger/oms-api'; -import { CanReturn, Receipt, ReceiptItem, ReceiptListItem } from '../models'; -import { CategoryQuestions, ProductCategory } from '../questions'; -import { KeyValue } from '@angular/common'; -import { ReturnCanReturnService } from './return-can-return.service'; -import { takeUntilAborted } from '@isa/common/data-access'; -import { z } from 'zod'; +} from "../schemas"; +import { firstValueFrom } from "rxjs"; +import { ReceiptService } from "@generated/swagger/oms-api"; +import { CanReturn, Receipt, ReceiptItem, ReceiptListItem } from "../models"; +import { CategoryQuestions, ProductCategory } from "../questions"; +import { KeyValue } from "@angular/common"; +import { ReturnCanReturnService } from "./return-can-return.service"; +import { takeUntilAborted } from "@isa/common/data-access"; +import { z } from "zod"; /** * Service responsible for managing receipt return details and operations. @@ -22,7 +22,7 @@ import { z } from 'zod'; * - Query receipts by customer email * - Get available product categories for returns */ -@Injectable({ providedIn: 'root' }) +@Injectable({ providedIn: "root" }) export class ReturnDetailsService { #receiptService = inject(ReceiptService); #returnCanReturnService = inject(ReturnCanReturnService); @@ -38,13 +38,17 @@ export class ReturnDetailsService { * @throws Will throw an error if the return check fails or is aborted. */ async canReturn( - { item, category }: { item: ReceiptItem; category: ProductCategory }, + { + receiptItemId, + quantity, + category, + }: { receiptItemId: number; quantity: number; category: ProductCategory }, abortSignal?: AbortSignal, ): Promise { const returnReceiptValues: ReturnReceiptValues = { - quantity: item.quantity.quantity, + quantity, receiptItem: { - id: item.id, + id: receiptItemId, }, category, }; @@ -102,7 +106,7 @@ export class ReturnDetailsService { const res = await firstValueFrom(req$); if (res.error || !res.result) { - throw new Error(res.message || 'Failed to fetch return details'); + throw new Error(res.message || "Failed to fetch return details"); } return res.result as Receipt; @@ -137,7 +141,7 @@ export class ReturnDetailsService { let req$ = this.#receiptService.ReceiptQueryReceipt({ queryToken: { input: { qs: email }, - filter: { receipt_type: '1;128;1024' }, + filter: { receipt_type: "1;128;1024" }, }, }); @@ -147,7 +151,7 @@ export class ReturnDetailsService { const res = await firstValueFrom(req$); if (res.error || !res.result) { - throw new Error(res.message || 'Failed to fetch return items by email'); + throw new Error(res.message || "Failed to fetch return items by email"); } return res.result as ReceiptListItem[]; diff --git a/libs/oms/data-access/src/lib/stores/return-details.store.ts b/libs/oms/data-access/src/lib/stores/return-details.store.ts index 709d8024c..7d4f5d739 100644 --- a/libs/oms/data-access/src/lib/stores/return-details.store.ts +++ b/libs/oms/data-access/src/lib/stores/return-details.store.ts @@ -1,13 +1,12 @@ -import { computed, inject, resource, untracked } from '@angular/core'; +import { computed, inject, resource } from "@angular/core"; import { CanReturn, ProductCategory, Receipt, ReceiptItem, ReturnDetailsService, -} from '@isa/oms/data-access'; +} from "@isa/oms/data-access"; import { - getState, patchState, signalStore, type, @@ -15,20 +14,18 @@ import { withMethods, withProps, withState, -} from '@ngrx/signals'; -import { setEntity, withEntities, entityConfig } from '@ngrx/signals/entities'; +} from "@ngrx/signals"; +import { setEntity, withEntities, entityConfig } from "@ngrx/signals/entities"; import { canReturnReceiptItem, getReceiptItemQuantity, getReceiptItemProductCategory, receiptItemHasCategory, -} from '../helpers/return-process'; -import { SessionStorageProvider } from '@isa/core/storage'; -import { logger } from '@isa/core/logging'; -import { clone } from 'lodash'; + getReceiptItemReturnedQuantity, +} from "../helpers/return-process"; +import { logger } from "@isa/core/logging"; interface ReturnDetailsState { - _storageId: number | undefined; _selectedItemIds: number[]; selectedProductCategory: Record; selectedQuantity: Record; @@ -36,7 +33,6 @@ interface ReturnDetailsState { } const initialState: ReturnDetailsState = { - _storageId: undefined, _selectedItemIds: [], selectedProductCategory: {}, selectedQuantity: {}, @@ -45,40 +41,15 @@ const initialState: ReturnDetailsState = { export const receiptConfig = entityConfig({ entity: type(), - collection: 'receipts', + collection: "receipts", }); export const ReturnDetailsStore = signalStore( - { providedIn: 'root' }, withState(initialState), withEntities(receiptConfig), withProps(() => ({ - _logger: logger(() => ({ store: 'ReturnDetailsStore' })), + _logger: logger(() => ({ store: "ReturnDetailsStore" })), _returnDetailsService: inject(ReturnDetailsService), - _storage: inject(SessionStorageProvider), - })), - withMethods((store) => ({ - _storageKey: () => `ReturnDetailsStore:${store._storageId()}`, - })), - withMethods((store) => ({ - _storeState: () => { - const state = getState(store); - if (!store._storageId) { - return; - } - store._storage.set(store._storageKey(), state); - store._logger.debug('State stored:', () => state); - }, - _restoreState: async () => { - const data = await store._storage.get(store._storageKey()); - if (data) { - patchState(store, data); - store._logger.debug('State restored:', () => ({ data })); - } else { - patchState(store, { ...initialState, _storageId: store._storageId() }); - store._logger.debug('No state found, initialized with default state'); - } - }, })), withComputed((store) => ({ items: computed>(() => @@ -86,43 +57,56 @@ export const ReturnDetailsStore = signalStore( .receiptsEntities() .map((receipt) => receipt.items) .flat() - .map((container) => { - const item = container.data; - if (!item) { - const err = new Error('Item data is undefined'); - store._logger.error('Item data is undefined', err, () => ({ - item: container, - })); - throw err; - } - - const itemData = clone(item); - - const quantityMap = store.selectedQuantity(); - - if (quantityMap[itemData.id]) { - itemData.quantity = { quantity: quantityMap[itemData.id] }; - } else { - const quantity = getReceiptItemQuantity(itemData); - if (!itemData.quantity) { - itemData.quantity = { quantity }; - } else { - itemData.quantity.quantity = quantity; - } - } - - if (!itemData.features) { - itemData.features = {}; - } - - itemData.features['category'] = - store.selectedProductCategory()[itemData.id] || - getReceiptItemProductCategory(itemData); - - return itemData; - }), + .map((container) => container.data!), ), })), + withComputed((store) => ({ + availableQuantityMap: computed(() => { + const items = store.items(); + const availableQuantity: Record = {}; + + items.forEach((item) => { + const itemId = item.id; + const quantity = getReceiptItemQuantity(item); + const returnedQuantity = getReceiptItemReturnedQuantity(item); + availableQuantity[itemId] = quantity - returnedQuantity; + }); + + return availableQuantity; + }), + + itemCategoryMap: computed(() => { + const items = store.items(); + const categoryMap: Record = {}; + + items.forEach((item) => { + const itemId = item.id; + const selectedCategory = store.selectedProductCategory()[itemId]; + const category = getReceiptItemProductCategory(item); + categoryMap[itemId] = selectedCategory ?? category; + }); + + return categoryMap; + }), + })), + + withComputed((store) => ({ + selectedQuantityMap: computed(() => { + const items = store.items(); + const selectedQuantity: Record = {}; + + items.forEach((item) => { + const itemId = item.id; + const quantity = + store.selectedQuantity()[itemId] || + store.availableQuantityMap()[itemId]; + selectedQuantity[itemId] = quantity; + }); + + return selectedQuantity; + }), + })), + withComputed((store) => ({ selectedItemIds: computed(() => { const selectedIds = store._selectedItemIds(); @@ -130,7 +114,7 @@ export const ReturnDetailsStore = signalStore( return selectedIds.filter((id) => { const canReturnResult = canReturn[id]?.result; - return typeof canReturnResult === 'boolean' ? canReturnResult : true; + return typeof canReturnResult === "boolean" ? canReturnResult : true; }); }), })), @@ -167,8 +151,8 @@ export const ReturnDetailsStore = signalStore( { receiptId: request }, abortSignal, ); + patchState(store, setEntity(receipt, receiptConfig)); - store._storeState(); return receipt; }, }), @@ -182,18 +166,21 @@ export const ReturnDetailsStore = signalStore( return undefined; } + const receiptItemId = item.id; + const quantity = store.selectedQuantityMap()[receiptItemId]; + const category = store.itemCategoryMap()[receiptItemId]; + return { - item: item, - category: - store.selectedProductCategory()[item.id] || - getReceiptItemProductCategory(item), + receiptItemId, + quantity, + category, }; }, loader: async ({ request, abortSignal }) => { if (request === undefined) { return undefined; } - const key = `${request.item.id}:${request.category}`; + const key = `${request.receiptItemId}:${request.category}`; if (store.canReturn()[key]) { return store.canReturn()[key]; @@ -207,7 +194,6 @@ export const ReturnDetailsStore = signalStore( canReturn: { ...store.canReturn(), [key]: res }, }); - store._storeState(); return res; }, }), @@ -248,37 +234,25 @@ export const ReturnDetailsStore = signalStore( })), withMethods((store) => ({ - selectStorage: (id: number) => { - untracked(() => { - patchState(store, { _storageId: id }); - store._restoreState(); - store._storeState(); - store._logger.debug('Storage ID set:', () => ({ id })); - }); - }, addSelectedItems(itemIds: number[]) { const currentIds = store.selectedItemIds(); const newIds = Array.from(new Set([...currentIds, ...itemIds])); patchState(store, { _selectedItemIds: newIds }); - store._storeState(); }, removeSelectedItems(itemIds: number[]) { const currentIds = store.selectedItemIds(); const newIds = currentIds.filter((id) => !itemIds.includes(id)); patchState(store, { _selectedItemIds: newIds }); - store._storeState(); }, async setProductCategory(itemId: number, category: ProductCategory) { const currentCategory = store.selectedProductCategory(); const newCategory = { ...currentCategory, [itemId]: category }; patchState(store, { selectedProductCategory: newCategory }); - store._storeState(); }, setQuantity(itemId: number, quantity: number) { const currentQuantity = store.selectedQuantity(); const newQuantity = { ...currentQuantity, [itemId]: quantity }; patchState(store, { selectedQuantity: newQuantity }); - store._storeState(); }, })), ); diff --git a/libs/oms/data-access/src/lib/stores/return-process.store.spec.ts b/libs/oms/data-access/src/lib/stores/return-process.store.spec.ts index e4f250362..0cf81a2df 100644 --- a/libs/oms/data-access/src/lib/stores/return-process.store.spec.ts +++ b/libs/oms/data-access/src/lib/stores/return-process.store.spec.ts @@ -1,69 +1,70 @@ -import { createServiceFactory } from '@ngneat/spectator/jest'; -import { ReturnProcessStore } from './return-process.store'; -import { IDBStorageProvider } from '@isa/core/storage'; -import { ProcessService } from '@isa/core/process'; -import { patchState } from '@ngrx/signals'; -import { setAllEntities, setEntity } from '@ngrx/signals/entities'; -import { unprotected } from '@ngrx/signals/testing'; -import { Product, ReturnProcess } from '../models'; -import { CreateReturnProcessError } from '../errors/return-process'; +import { createServiceFactory } from "@ngneat/spectator/jest"; +import { ReturnProcessStore } from "./return-process.store"; +import { IDBStorageProvider } from "@isa/core/storage"; +import { ProcessService } from "@isa/core/process"; +import { patchState } from "@ngrx/signals"; +import { setAllEntities, setEntity } from "@ngrx/signals/entities"; +import { unprotected } from "@ngrx/signals/testing"; +import { Product, ReturnProcess } from "../models"; +import { CreateReturnProcessError } from "../errors/return-process"; +import { ProductCategory } from "../questions"; -const TEST_ITEMS: Record = { +const TEST_ITEMS: Record = { 1: { id: 1, - actions: [{ key: 'canReturn', value: 'true' }], + actions: [{ key: "canReturn", value: "true" }], product: { - ean: '1234567890', - format: 'TB', - formatDetail: 'Taschenbuch', + ean: "1234567890", + format: "TB", + formatDetail: "Taschenbuch", } as Product, quantity: { quantity: 1 }, - receiptNumber: 'R-001', + receiptNumber: "R-001", }, 2: { id: 2, - actions: [{ key: 'canReturn', value: 'false' }], + actions: [{ key: "canReturn", value: "false" }], product: { - ean: '0987654321', - format: 'GEB', - formatDetail: 'Buch', + ean: "0987654321", + format: "GEB", + formatDetail: "Buch", } as Product, quantity: { quantity: 1 }, - receiptNumber: 'R-002', + receiptNumber: "R-002", }, 3: { id: 3, - actions: [{ key: 'canReturn', value: 'true' }], + actions: [{ key: "canReturn", value: "true" }], product: { - ean: '1122334455', - format: 'AU', - formatDetail: 'Audio', + ean: "1122334455", + format: "AU", + formatDetail: "Audio", } as Product, quantity: { quantity: 1 }, - receiptNumber: 'R-003', + receiptNumber: "R-003", }, }; -describe('ReturnProcessStore', () => { +describe("ReturnProcessStore", () => { const createService = createServiceFactory({ service: ReturnProcessStore, mocks: [IDBStorageProvider, ProcessService], }); - describe('Initialization', () => { - it('should create an instance of ReturnProcessStore', () => { + describe("Initialization", () => { + it("should create an instance of ReturnProcessStore", () => { const spectator = createService(); expect(spectator.service).toBeTruthy(); }); - it('should have a nextId computed property', () => { + it("should have a nextId computed property", () => { const spectator = createService(); expect(spectator.service.nextId()).toBe(1); // Assuming no entities exist initially }); }); - describe('Entity Management', () => { - it('should remove all entities by process id', () => { + describe("Entity Management", () => { + it("should remove all entities by process id", () => { const spectator = createService(); const store = spectator.service; @@ -75,9 +76,10 @@ describe('ReturnProcessStore', () => { processId: 1, receiptId: 1, receiptItem: TEST_ITEMS[1], - receiptDate: '', + receiptDate: "", answers: {}, - productCategory: undefined, + productCategory: ProductCategory.BookCalendar, + quantity: 1, returnReceipt: undefined, }, { @@ -85,9 +87,10 @@ describe('ReturnProcessStore', () => { processId: 2, receiptId: 2, receiptItem: TEST_ITEMS[2], - receiptDate: '', + receiptDate: "", answers: {}, - productCategory: undefined, + productCategory: ProductCategory.BookCalendar, + quantity: 1, returnReceipt: undefined, }, { @@ -95,9 +98,10 @@ describe('ReturnProcessStore', () => { processId: 1, receiptId: 3, receiptItem: TEST_ITEMS[3], - receiptDate: '', + receiptDate: "", answers: {}, - productCategory: undefined, + productCategory: ProductCategory.BookCalendar, + quantity: 1, returnReceipt: undefined, }, ] as ReturnProcess[]), @@ -108,7 +112,7 @@ describe('ReturnProcessStore', () => { expect(store.entities()[0].processId).toBe(2); }); - it('should set an answer for a given entity', () => { + it("should set an answer for a given entity", () => { const spectator = createService(); const store = spectator.service; @@ -120,19 +124,20 @@ describe('ReturnProcessStore', () => { processId: 1, receiptId: 1, receiptItem: TEST_ITEMS[1], - receiptDate: '', + receiptDate: "", answers: {}, - productCategory: undefined, + productCategory: ProductCategory.BookCalendar, + quantity: 1, returnReceipt: undefined, }, ] as ReturnProcess[]), ); - store.setAnswer(1, 'question1', 'answer1'); - expect(store.entityMap()[1].answers['question1']).toBe('answer1'); + store.setAnswer(1, "question1", "answer1"); + expect(store.entityMap()[1].answers["question1"]).toBe("answer1"); }); - it('should remove an answer for a given entity', () => { + it("should remove an answer for a given entity", () => { const spectator = createService(); const store = spectator.service; @@ -141,25 +146,26 @@ describe('ReturnProcessStore', () => { setEntity({ id: 1, processId: 1, - answers: { question1: 'answer1', question2: 'answer2' } as Record< + answers: { question1: "answer1", question2: "answer2" } as Record< string, unknown >, receiptDate: new Date().toJSON(), receiptItem: TEST_ITEMS[1], receiptId: 123, - productCategory: undefined, + productCategory: ProductCategory.BookCalendar, + quantity: 1, returnReceipt: undefined, } as ReturnProcess), ); - store.removeAnswer(1, 'question1'); - expect(store.entityMap()[1].answers['question1']).toBeUndefined(); + store.removeAnswer(1, "question1"); + expect(store.entityMap()[1].answers["question1"]).toBeUndefined(); }); }); - describe('Process Management', () => { - it('should initialize a new return process', () => { + describe("Process Management", () => { + it("should initialize a new return process", () => { const spectator = createService(); const store = spectator.service; @@ -169,28 +175,44 @@ describe('ReturnProcessStore', () => { { receipt: { id: 1, - printedDate: '', + printedDate: "", items: [], - buyer: { buyerNumber: '' }, + buyer: { buyerNumber: "" }, }, - items: [TEST_ITEMS[1]], + items: [ + { + receiptItem: TEST_ITEMS[1], + quantity: 1, + category: ProductCategory.BookCalendar, + }, + ], }, { receipt: { id: 2, - printedDate: '', + printedDate: "", items: [], - buyer: { buyerNumber: '' }, + buyer: { buyerNumber: "" }, }, - items: [TEST_ITEMS[3]], + items: [ + { + receiptItem: TEST_ITEMS[3], + quantity: 1, + category: ProductCategory.BookCalendar, + }, + ], }, ], }); expect(store.entities()).toHaveLength(2); + expect(store.entities()[0].productCategory).toBe( + ProductCategory.BookCalendar, + ); + expect(store.entities()[0].quantity).toBe(1); }); - it('should throw an error if no returnable items are found', () => { + it("should throw an error if no returnable items are found", () => { const spectator = createService(); const store = spectator.service; @@ -201,18 +223,24 @@ describe('ReturnProcessStore', () => { { receipt: { id: 2, - printedDate: '', + printedDate: "", items: [], - buyer: { buyerNumber: '' }, + buyer: { buyerNumber: "" }, }, - items: [TEST_ITEMS[2]], // Non-returnable item + items: [ + { + receiptItem: TEST_ITEMS[2], // Non-returnable item + quantity: 1, + category: ProductCategory.BookCalendar, + }, + ], }, ], }); }).toThrow(CreateReturnProcessError); }); - it('should throw an error if the number of returnable items does not match the total items', () => { + it("should throw an error if the number of returnable items does not match the total items", () => { const spectator = createService(); const store = spectator.service; @@ -223,11 +251,27 @@ describe('ReturnProcessStore', () => { { receipt: { id: 3, - printedDate: '', + printedDate: "", items: [], - buyer: { buyerNumber: '' }, + buyer: { buyerNumber: "" }, }, - items: [TEST_ITEMS[1], TEST_ITEMS[2], TEST_ITEMS[3]], + items: [ + { + receiptItem: TEST_ITEMS[1], + quantity: 1, + category: ProductCategory.BookCalendar, + }, + { + receiptItem: TEST_ITEMS[2], + quantity: 1, + category: ProductCategory.BookCalendar, + }, + { + receiptItem: TEST_ITEMS[3], + quantity: 1, + category: ProductCategory.BookCalendar, + }, + ], }, ], }); diff --git a/libs/oms/data-access/src/lib/stores/return-process.store.ts b/libs/oms/data-access/src/lib/stores/return-process.store.ts index 9575ea3ef..7ba005e98 100644 --- a/libs/oms/data-access/src/lib/stores/return-process.store.ts +++ b/libs/oms/data-access/src/lib/stores/return-process.store.ts @@ -5,29 +5,37 @@ import { withHooks, withMethods, withProps, -} from '@ngrx/signals'; +} from "@ngrx/signals"; import { withEntities, setAllEntities, updateEntity, -} from '@ngrx/signals/entities'; -import { IDBStorageProvider, withStorage } from '@isa/core/storage'; -import { computed, effect, inject } from '@angular/core'; -import { ProcessService } from '@isa/core/process'; -import { Receipt, ReceiptItem, ReturnProcess } from '../models'; +} from "@ngrx/signals/entities"; +import { IDBStorageProvider, withStorage } from "@isa/core/storage"; +import { computed, effect, inject } from "@angular/core"; +import { ProcessService } from "@isa/core/process"; +import { Receipt, ReceiptItem, ReturnProcess } from "../models"; import { CreateReturnProcessError, CreateReturnProcessErrorReason, -} from '../errors/return-process'; -import { logger } from '@isa/core/logging'; -import { canReturnReceiptItem } from '../helpers/return-process'; +} from "../errors/return-process"; +import { logger } from "@isa/core/logging"; +import { canReturnReceiptItem } from "../helpers/return-process"; +import { ProductCategory } from "../questions"; /** * Interface representing the parameters required to start a return process. */ export type StartProcess = { processId: number; - returns: { receipt: Receipt; items: ReceiptItem[] }[]; + returns: { + receipt: Receipt; + items: { + receiptItem: ReceiptItem; + quantity: number; + category: ProductCategory; + }[]; + }[]; }; /** @@ -55,12 +63,12 @@ export type StartProcess = { * - Throws a MismatchReturnableItemsError if the number of returnable items does not match the expected count. */ export const ReturnProcessStore = signalStore( - { providedIn: 'root' }, - withStorage('return-process', IDBStorageProvider), + { providedIn: "root" }, + withStorage("return-process", IDBStorageProvider), withEntities(), withProps(() => ({ _logger: logger(() => ({ - store: 'ReturnProcessStore', + store: "ReturnProcessStore", })), })), withComputed((store) => ({ @@ -142,6 +150,7 @@ export const ReturnProcessStore = signalStore( const returnableItems = params.returns .flatMap((r) => r.items) + .map((item) => item.receiptItem) .filter(canReturnReceiptItem); if (returnableItems.length === 0) { @@ -170,9 +179,10 @@ export const ReturnProcessStore = signalStore( id: nextId + entities.length, processId: params.processId, receiptId: receipt.id, - productCategory: item.features?.['category'], + productCategory: item.category, + quantity: item.quantity, receiptDate: receipt.printedDate, - receiptItem: item, + receiptItem: item.receiptItem, answers: {}, }); } diff --git a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html index f0308112c..c0898367a 100644 --- a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html +++ b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.html @@ -5,7 +5,7 @@ @for (quantity of quantityDropdownValues(); track quantity) { diff --git a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.spec.ts b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.spec.ts index a56e69208..0b64af5fb 100644 --- a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.spec.ts +++ b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.spec.ts @@ -1,50 +1,52 @@ -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; -import { MockDirective } from 'ng-mocks'; +import { createComponentFactory, Spectator } from "@ngneat/spectator/jest"; +import { MockDirective } from "ng-mocks"; import { ReceiptItem, ReturnDetailsService, ReturnDetailsStore, -} from '@isa/oms/data-access'; +} from "@isa/oms/data-access"; -import { ProductImageDirective } from '@isa/shared/product-image'; -import { ReturnDetailsOrderGroupItemControlsComponent } from '../return-details-order-group-item-controls/return-details-order-group-item-controls.component'; -import { CheckboxComponent } from '@isa/ui/input-controls'; -import { signal } from '@angular/core'; +import { ProductImageDirective } from "@isa/shared/product-image"; +import { ReturnDetailsOrderGroupItemControlsComponent } from "../return-details-order-group-item-controls/return-details-order-group-item-controls.component"; +import { CheckboxComponent } from "@isa/ui/input-controls"; +import { signal } from "@angular/core"; // Helper function to create mock ReceiptItem data const createMockItem = ( ean: string, canReturn: boolean, - name = 'Test Product', - category = 'BOOK', // Add default category that's not 'unknown' + name = "Test Product", + category = "BOOK", // Add default category that's not 'unknown' + availableQuantity = 2, + selectedQuantity = 1, ): ReceiptItem => ({ id: 123, - receiptNumber: 'R-123456', // Add the required receiptNumber property - quantity: { quantity: 1 }, + receiptNumber: "R-123456", + quantity: { quantity: availableQuantity }, price: { - value: { value: 19.99, currency: 'EUR' }, + value: { value: 19.99, currency: "EUR" }, vat: { inPercent: 19 }, }, product: { ean: ean, name: name, - contributors: 'Test Author', - format: 'HC', - formatDetail: 'Hardcover', - manufacturer: 'Test Publisher', - publicationDate: '2024-01-01T00:00:00Z', - catalogProductNumber: '1234567890', - volume: '1', + contributors: "Test Author", + format: "HC", + formatDetail: "Hardcover", + manufacturer: "Test Publisher", + publicationDate: "2024-01-01T00:00:00Z", + catalogProductNumber: "1234567890", + volume: "1", }, - actions: [{ key: 'canReturn', value: String(canReturn) }], - features: { category: category }, // Add the features property with category + actions: [{ key: "canReturn", value: String(canReturn) }], + features: { category: category }, }) as ReceiptItem; -describe('ReturnDetailsOrderGroupItemControlsComponent', () => { +describe("ReturnDetailsOrderGroupItemControlsComponent", () => { let spectator: Spectator; - const mockItemSelectable = createMockItem('1234567890123', true); + const mockItemSelectable = createMockItem("1234567890123", true); const mockIsSelectable = signal(true); const mockGetItemSelectted = signal(false); @@ -52,6 +54,11 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => { isLoading: signal(true), }; + // Mocks for availableQuantityMap and selectedQuantityMap + const mockAvailableQuantityMap = { [mockItemSelectable.id]: 2 }; + const mockSelectedQuantityMap = { [mockItemSelectable.id]: 1 }; + const mockItemCategoryMap = { [mockItemSelectable.id]: "BOOK" }; + function resetMocks() { mockIsSelectable.set(true); mockGetItemSelectted.set(false); @@ -68,12 +75,16 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => { isSelectable: jest.fn(() => mockIsSelectable), getItemSelected: jest.fn(() => mockGetItemSelectted), canReturnResource: jest.fn(() => mockCanReturnResource), + availableQuantityMap: jest.fn(() => mockAvailableQuantityMap), + selectedQuantityMap: jest.fn(() => mockSelectedQuantityMap), + itemCategoryMap: jest.fn(() => mockItemCategoryMap), + setProductCategory: jest.fn(), + setQuantity: jest.fn(), + addSelectedItems: jest.fn(), + removeSelectedItems: jest.fn(), }, }, ], - // Spectator automatically stubs standalone dependencies like ItemRowComponent, CheckboxComponent etc. - // We don't need deep interaction, just verify the host component renders correctly. - // If specific interactions were needed, we could provide mocks or use overrideComponents. overrideComponents: [ [ ReturnDetailsOrderGroupItemControlsComponent, @@ -85,50 +96,41 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => { }, ], ], - detectChanges: false, // Control initial detection manually + detectChanges: false, }); beforeEach(() => { - // Default setup with a selectable item spectator = createComponent({ props: { - item: mockItemSelectable, // Use signal for input + item: mockItemSelectable, }, }); }); afterEach(() => { - resetMocks(); // Reset mocks after each test + resetMocks(); }); - it('should create', () => { - // Arrange - spectator.detectChanges(); // Trigger initial render - - // Assert + it("should create", () => { + spectator.detectChanges(); expect(spectator.component).toBeTruthy(); }); - it('should display the checkbox when item is selectable', () => { - // Arrange - mockCanReturnResource.isLoading.set(false); // Simulate the resource being ready - mockIsSelectable.set(true); // Simulate the item being selectable + it("should display the checkbox when item is selectable and not loading", () => { + mockCanReturnResource.isLoading.set(false); + mockIsSelectable.set(true); spectator.detectChanges(); - // Assert expect(spectator.component.selectable()).toBe(true); - const checkbox = spectator.query(CheckboxComponent); - expect(checkbox).toBeTruthy(); + expect(spectator.query(CheckboxComponent)).toBeTruthy(); expect( spectator.query(`input[data-what="return-item-checkbox"]`), ).toExist(); }); - it('should NOT display the checkbox when item is not selectable', () => { - // Arrange - mockIsSelectable.set(false); // Simulate the item not being selectable - spectator.detectChanges(); - spectator.detectComponentChanges(); - // Assert + it("should NOT display the checkbox when item is not selectable", () => { + mockIsSelectable.set(false); + mockCanReturnResource.isLoading.set(false); + spectator.detectChanges(); expect(spectator.component.selectable()).toBe(false); expect( spectator.query(`input[data-what="return-item-checkbox"]`), @@ -136,27 +138,73 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => { expect(spectator.query(CheckboxComponent)).toBeFalsy(); }); - it('should be false when no canReturn action is present', () => { - // Arrange - const item = { ...createMockItem('0001', true), actions: [] }; - spectator.setInput('item', item as any); - - // Act + it("should show spinner when canReturnResource is loading", () => { + mockCanReturnResource.isLoading.set(true); spectator.detectChanges(); + expect( + spectator.query('ui-icon-button[data-what="load-spinner"]'), + ).toExist(); + }); - // Assert + it("should render correct quantity dropdown values", () => { + spectator.detectChanges(); + expect(spectator.component.quantityDropdownValues()).toEqual([1, 2]); + }); + + it("should call setQuantity when dropdown value changes", () => { + const store = spectator.inject(ReturnDetailsStore); + const spy = jest.spyOn(store, "setQuantity"); + spectator.detectChanges(); + // Simulate dropdown value change + spectator.component.setQuantity(2); + expect(spy).toHaveBeenCalledWith(mockItemSelectable.id, 2); + }); + + it("should call setProductCategory when product category changes", () => { + const store = spectator.inject(ReturnDetailsStore); + const spy = jest.spyOn(store, "setProductCategory"); + spectator.detectChanges(); + spectator.component.setProductCategory("Buch/Kalender"); + expect(spy).toHaveBeenCalledWith(mockItemSelectable.id, "Buch/Kalender"); + }); + + it("should call addSelectedItems when setSelected(true) is called", () => { + const store = spectator.inject(ReturnDetailsStore); + const spy = jest.spyOn(store, "addSelectedItems"); + spectator.detectChanges(); + spectator.component.setSelected(true); + expect(spy).toHaveBeenCalledWith([mockItemSelectable.id]); + }); + + it("should call removeSelectedItems when setSelected(false) is called", () => { + const store = spectator.inject(ReturnDetailsStore); + const spy = jest.spyOn(store, "removeSelectedItems"); + spectator.detectChanges(); + spectator.component.setSelected(false); + expect(spy).toHaveBeenCalledWith([mockItemSelectable.id]); + }); + + it("should be false when no canReturn action is present", () => { + const item = { ...createMockItem("0001", true), actions: [] }; + spectator.setInput("item", item as any); + spectator.detectChanges(); expect(spectator.component.canReturnReceiptItem()).toBe(false); }); - it('should be false when canReturn action has falsy value', () => { - // Arrange - const item = createMockItem('0001', false); - spectator.setInput('item', item); - - // Act + it("should be false when canReturn action has falsy value", () => { + const item = createMockItem("0001", false); + spectator.setInput("item", item); spectator.detectChanges(); - - // Assert expect(spectator.component.canReturnReceiptItem()).toBe(false); }); + + it("should display correct selected quantity", () => { + spectator.detectChanges(); + expect(spectator.component.selectedQuantity()).toBe(1); + }); + + it("should display correct product category", () => { + spectator.detectChanges(); + expect(spectator.component.productCategory()).toBe("BOOK"); + }); }); diff --git a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.ts b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.ts index 33d0e8408..1e0e11830 100644 --- a/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.ts +++ b/libs/oms/feature/return-details/src/lib/return-details-order-group-item-controls/return-details-order-group-item-controls.component.ts @@ -5,30 +5,27 @@ import { inject, input, signal, -} from '@angular/core'; -import { provideLoggerContext } from '@isa/core/logging'; +} from "@angular/core"; +import { provideLoggerContext } from "@isa/core/logging"; import { canReturnReceiptItem, - getReceiptItemReturnedQuantity, - getReceiptItemProductCategory, - getReceiptItemQuantity, ProductCategory, ReceiptItem, ReturnDetailsService, ReturnDetailsStore, -} from '@isa/oms/data-access'; -import { IconButtonComponent } from '@isa/ui/buttons'; +} from "@isa/oms/data-access"; +import { IconButtonComponent } from "@isa/ui/buttons"; import { CheckboxComponent, DropdownButtonComponent, DropdownOptionComponent, -} from '@isa/ui/input-controls'; -import { FormsModule } from '@angular/forms'; +} from "@isa/ui/input-controls"; +import { FormsModule } from "@angular/forms"; @Component({ - selector: 'oms-feature-return-details-order-group-item-controls', - templateUrl: './return-details-order-group-item-controls.component.html', - styleUrls: ['./return-details-order-group-item-controls.component.scss'], + selector: "oms-feature-return-details-order-group-item-controls", + templateUrl: "./return-details-order-group-item-controls.component.html", + styleUrls: ["./return-details-order-group-item-controls.component.scss"], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ @@ -40,7 +37,7 @@ import { FormsModule } from '@angular/forms'; ], providers: [ provideLoggerContext({ - component: 'ReturnDetailsOrderGroupItemControlsComponent', + component: "ReturnDetailsOrderGroupItemControlsComponent", }), ], }) @@ -66,38 +63,11 @@ export class ReturnDetailsOrderGroupItemControlsComponent { availableCategories = this.#returnDetailsService.availableCategories(); - /** - * Computes the quantity of the current receipt item that has already been returned. - * - * This value is derived from the item's return history and is used to indicate - * how many units have already been processed for return. - * - * @returns The number of units already returned for this receipt item. - */ - returnedQuantity = computed(() => { + selectedQuantity = computed(() => { const item = this.item(); - return getReceiptItemReturnedQuantity(item); + return this.#store.selectedQuantityMap()[item.id]; }); - /** - * Computes the total quantity for the current receipt item. - * Represents the original quantity as recorded in the receipt. - * - * @returns The total quantity for the item. - */ - quantity = computed(() => { - const item = this.item(); - return getReceiptItemQuantity(item); - }); - - /** - * Computes the quantity of the item that is still available for return. - * Calculated as the difference between the total quantity and the returned quantity. - * - * @returns The number of units available to be returned. - */ - availableQuantity = computed(() => this.quantity() - this.returnedQuantity()); - /** * Generates the list of selectable quantities for the dropdown. * The values range from 1 up to the available quantity. @@ -105,13 +75,14 @@ export class ReturnDetailsOrderGroupItemControlsComponent { * @returns An array of selectable quantity values. */ quantityDropdownValues = computed(() => { - const itemQuantity = this.availableQuantity(); + const item = this.item(); + const itemQuantity = this.#store.availableQuantityMap()[item.id]; return Array.from({ length: itemQuantity }, (_, i) => i + 1); }); productCategory = computed(() => { const item = this.item(); - return getReceiptItemProductCategory(item); + return this.#store.itemCategoryMap()[item.id]; }); selectable = this.#store.isSelectable(this.item); @@ -127,8 +98,9 @@ export class ReturnDetailsOrderGroupItemControlsComponent { } setQuantity(quantity: number | undefined) { + const item = this.item(); if (quantity === undefined) { - quantity = this.item().quantity.quantity; + quantity = this.#store.availableQuantityMap()[item.id]; } this.#store.setQuantity(this.item().id, quantity); } diff --git a/libs/oms/feature/return-details/src/lib/return-details-order-group-item/return-details-order-group-item.component.html b/libs/oms/feature/return-details/src/lib/return-details-order-group-item/return-details-order-group-item.component.html index ac7c35842..85f01a2ab 100644 --- a/libs/oms/feature/return-details/src/lib/return-details-order-group-item/return-details-order-group-item.component.html +++ b/libs/oms/feature/return-details/src/lib/return-details-order-group-item/return-details-order-group-item.component.html @@ -57,7 +57,7 @@ {{ i.product.manufacturer }} | {{ i.product.ean }}
- {{ i.product.publicationDate | date: 'dd. MMM yyyy' }} + {{ i.product.publicationDate | date: "dd. MMM yyyy" }}
@@ -73,11 +73,11 @@
} -@if (returnedQuantity() > 0 && itemQuantity() !== returnedQuantity()) { +@if (availableQuantity() !== quantity()) {
- Es wurden bereits {{ returnedQuantity() }} von {{ itemQuantity() }} Artikel - zurückgegeben. + Es wurden bereits {{ quantity() - availableQuantity() }} von + {{ quantity() }} Artikel zurückgegeben.
} diff --git a/libs/oms/feature/return-details/src/lib/return-details-order-group-item/return-details-order-group-item.component.ts b/libs/oms/feature/return-details/src/lib/return-details-order-group-item/return-details-order-group-item.component.ts index f32d7235f..69594cc8c 100644 --- a/libs/oms/feature/return-details/src/lib/return-details-order-group-item/return-details-order-group-item.component.ts +++ b/libs/oms/feature/return-details/src/lib/return-details-order-group-item/return-details-order-group-item.component.ts @@ -1,29 +1,28 @@ -import { CurrencyPipe, DatePipe, LowerCasePipe } from '@angular/common'; +import { CurrencyPipe, DatePipe, LowerCasePipe } from "@angular/common"; import { ChangeDetectionStrategy, Component, computed, inject, input, -} from '@angular/core'; -import { isaActionClose, ProductFormatIconGroup } from '@isa/icons'; +} from "@angular/core"; +import { isaActionClose, ProductFormatIconGroup } from "@isa/icons"; import { getReceiptItemAction, - getReceiptItemReturnedQuantity, getReceiptItemQuantity, ReceiptItem, ReturnDetailsStore, -} from '@isa/oms/data-access'; -import { ProductImageDirective } from '@isa/shared/product-image'; -import { ItemRowComponent } from '@isa/ui/item-rows'; -import { NgIconComponent, provideIcons } from '@ng-icons/core'; -import { ReturnDetailsOrderGroupItemControlsComponent } from '../return-details-order-group-item-controls/return-details-order-group-item-controls.component'; -import { ProductRouterLinkDirective } from '@isa/shared/product-router-link'; +} from "@isa/oms/data-access"; +import { ProductImageDirective } from "@isa/shared/product-image"; +import { ItemRowComponent } from "@isa/ui/item-rows"; +import { NgIconComponent, provideIcons } from "@ng-icons/core"; +import { ReturnDetailsOrderGroupItemControlsComponent } from "../return-details-order-group-item-controls/return-details-order-group-item-controls.component"; +import { ProductRouterLinkDirective } from "@isa/shared/product-router-link"; @Component({ - selector: 'oms-feature-return-details-order-group-item', - templateUrl: './return-details-order-group-item.component.html', - styleUrls: ['./return-details-order-group-item.component.scss'], + selector: "oms-feature-return-details-order-group-item", + templateUrl: "./return-details-order-group-item.component.html", + styleUrls: ["./return-details-order-group-item.component.scss"], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ @@ -82,7 +81,7 @@ export class ReturnDetailsOrderGroupItemComponent { */ canReturnMessage = computed(() => { const item = this.item(); - const canReturnAction = getReceiptItemAction(item, 'canReturn'); + const canReturnAction = getReceiptItemAction(item, "canReturn"); if (canReturnAction?.description) { return canReturnAction.description; @@ -90,30 +89,32 @@ export class ReturnDetailsOrderGroupItemComponent { const canReturnMessage = this.canReturn()?.message; - return canReturnMessage ?? ''; + return canReturnMessage ?? ""; }); /** - * Computes the quantity of the current receipt item that has already been returned. + * The original quantity of the item as recorded in the order. + * This value is retrieved from the store and represents the total number of units + * initially purchased for this receipt item. * - * This value is derived using the item's return history and is used to display - * how many units of this item have been processed for return so far. - * - * @returns The number of units already returned for this receipt item. + * @readonly + * @returns {number} The original quantity of the item in the order. */ - returnedQuantity = computed(() => { - const item = this.item(); - return getReceiptItemReturnedQuantity(item); - }); - - /** - * Computes the total quantity for the current receipt item. - * Represents the original quantity of the item as recorded in the receipt. - * - * @returns The total quantity for the item. - */ - itemQuantity = computed(() => { + quantity = computed(() => { const item = this.item(); return getReceiptItemQuantity(item); }); + + /** + * The currently available quantity of the item for return. + * This value is computed based on the item's current state and may be less than + * the original quantity if some units have already been returned or are otherwise unavailable. + * + * @readonly + * @returns {number} The number of units available for return. + */ + availableQuantity = computed(() => { + const item = this.item(); + return this.#store.availableQuantityMap()[item.id]; + }); } diff --git a/libs/oms/feature/return-details/src/lib/return-details.component.ts b/libs/oms/feature/return-details/src/lib/return-details.component.ts index 1ae500c1c..5efdd3baa 100644 --- a/libs/oms/feature/return-details/src/lib/return-details.component.ts +++ b/libs/oms/feature/return-details/src/lib/return-details.component.ts @@ -2,35 +2,34 @@ import { ChangeDetectionStrategy, Component, computed, - effect, inject, resource, -} from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { z } from 'zod'; +} from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { z } from "zod"; -import { NgIconComponent, provideIcons } from '@ng-icons/core'; -import { isaActionChevronLeft } from '@isa/icons'; -import { ButtonComponent } from '@isa/ui/buttons'; -import { injectActivatedProcessId } from '@isa/core/process'; -import { Location } from '@angular/common'; -import { ExpandableDirectives } from '@isa/ui/expandable'; -import { ProgressBarComponent } from '@isa/ui/progress-bar'; +import { NgIconComponent, provideIcons } from "@ng-icons/core"; +import { isaActionChevronLeft } from "@isa/icons"; +import { ButtonComponent } from "@isa/ui/buttons"; +import { injectActivatedProcessId } from "@isa/core/process"; +import { Location } from "@angular/common"; +import { ExpandableDirectives } from "@isa/ui/expandable"; +import { ProgressBarComponent } from "@isa/ui/progress-bar"; import { ReturnDetailsService, ReturnProcessStore, ReturnDetailsStore, -} from '@isa/oms/data-access'; -import { ReturnDetailsStaticComponent } from './return-details-static/return-details-static.component'; -import { ReturnDetailsLazyComponent } from './return-details-lazy/return-details-lazy.component'; -import { logger } from '@isa/core/logging'; -import { groupBy } from 'lodash'; +} from "@isa/oms/data-access"; +import { ReturnDetailsStaticComponent } from "./return-details-static/return-details-static.component"; +import { ReturnDetailsLazyComponent } from "./return-details-lazy/return-details-lazy.component"; +import { logger } from "@isa/core/logging"; +import { groupBy } from "lodash"; @Component({ - selector: 'oms-feature-return-details', - templateUrl: './return-details.component.html', - styleUrls: ['./return-details.component.scss'], + selector: "oms-feature-return-details", + templateUrl: "./return-details.component.html", + styleUrls: ["./return-details.component.scss"], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ReturnDetailsStaticComponent, @@ -40,11 +39,11 @@ import { groupBy } from 'lodash'; ExpandableDirectives, ProgressBarComponent, ], - providers: [provideIcons({ isaActionChevronLeft })], + providers: [provideIcons({ isaActionChevronLeft }), ReturnDetailsStore], }) export class ReturnDetailsComponent { #logger = logger(() => ({ - component: 'ReturnDetailsComponent', + component: "ReturnDetailsComponent", itemId: this.receiptId(), processId: this.processId(), params: this.params(), @@ -66,21 +65,17 @@ export class ReturnDetailsComponent { receiptId = computed(() => { const params = this.params(); if (params) { - return z.coerce.number().parse(params['receiptId']); + return z.coerce.number().parse(params["receiptId"]); } - throw new Error('No receiptId found in route params'); + throw new Error("No receiptId found in route params"); }); - // Effect resets the Store's state when the receiptId changes - // This ensures that the store is always in sync with the current receiptId - receiptIdEffect = effect(() => this.#store.selectStorage(this.receiptId())); - receiptResource = this.#store.receiptResource(this.receiptId); customerReceiptsResource = resource({ request: this.receiptResource.value, loader: async ({ request, abortSignal }) => { - console.log('Fetching customer receipts for:', request); + console.log("Fetching customer receipts for:", request); const email = request?.buyer?.communicationDetails?.email; if (!email) { return []; @@ -101,15 +96,17 @@ export class ReturnDetailsComponent { startProcess() { if (!this.canStartProcess()) { this.#logger.warn( - 'Cannot start process: No items selected or no process ID', + "Cannot start process: No items selected or no process ID", ); return; } const processId = this.processId(); const selectedItems = this.#store.selectedItems(); + const selectedQuantites = this.#store.selectedQuantityMap(); + const selectedProductCategories = this.#store.itemCategoryMap(); - this.#logger.info('Starting return process', () => ({ + this.#logger.info("Starting return process", () => ({ processId: processId, selectedItems: selectedItems.map((item) => item.id), })); @@ -127,11 +124,18 @@ export class ReturnDetailsComponent { const returns = Object.entries(itemsGrouptByReceiptId).map( ([receiptId, items]) => ({ receipt: receipts[Number(receiptId)], - items, + items: items.map((item) => { + const receiptItem = item; + return { + receiptItem, + quantity: selectedQuantites[receiptItem.id], + category: selectedProductCategories[receiptItem.id], + }; + }), }), ); - this.#logger.info('Starting return process with returns', () => ({ + this.#logger.info("Starting return process with returns", () => ({ processId, returns, })); @@ -141,7 +145,7 @@ export class ReturnDetailsComponent { returns, }); - this._router.navigate(['../../', 'process'], { + this._router.navigate(["../../", "process"], { relativeTo: this._activatedRoute, }); }