From 41128e714c3e1fd6807e98ed33aa0cb3e226d5e7 Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Mon, 3 Aug 2026 23:30:05 +0800 Subject: [PATCH 01/10] chore: bump @vality/domain-proto version to 2.0.2-99c91c9.0 --- package-lock.json | 6 ++++-- package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8def5ab8..c90320afb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@ngneat/input-mask": "^6.1.0", "@sentry/angular": "^10.68.0", "@tailwindcss/postcss": "^4.3.1", - "@vality/domain-proto": "^2.0.2-239638b.0", + "@vality/domain-proto": "^2.0.2-99c91c9.0", "@vality/fistful-proto": "^2.0.1-9864622.0", "@vality/machinegun-proto": "^1.0.1-273f2f3.0", "@vality/magista-proto": "^2.0.2-bd58cea.0", @@ -5722,7 +5722,9 @@ ] }, "node_modules/@vality/domain-proto": { - "version": "2.0.2-b07c077.0", + "version": "2.0.2-99c91c9.0", + "resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-2.0.2-99c91c9.0.tgz", + "integrity": "sha512-tLLIJjlnldBnhq+qDMDnLS1lYzDCcFRFSd/eyCliW8d6Ifxg/wvddUZZlNjT3CyWx6Tl/OpaHWbP8eE8Eeg03A==", "license": "Apache-2.0" }, "node_modules/@vality/fistful-proto": { diff --git a/package.json b/package.json index 72527def3..7f1ffad61 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@ngneat/input-mask": "^6.1.0", "@sentry/angular": "^10.68.0", "@tailwindcss/postcss": "^4.3.1", - "@vality/domain-proto": "^2.0.2-239638b.0", + "@vality/domain-proto": "^2.0.2-99c91c9.0", "@vality/fistful-proto": "^2.0.1-9864622.0", "@vality/machinegun-proto": "^1.0.1-273f2f3.0", "@vality/magista-proto": "^2.0.2-bd58cea.0", From 6a51f0b1194b52210d24bc21ea9a39a1206b7d4a Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:18:13 +0800 Subject: [PATCH 02/10] refactor: implement reactive form-based invoice template creation with automatic link generation --- src/app/payments/payments.component.html | 2 +- ...ate-invoice-template-dialog.component.html | 71 ++++--- ...reate-invoice-template-dialog.component.ts | 198 +++++++----------- 3 files changed, 115 insertions(+), 156 deletions(-) diff --git a/src/app/payments/payments.component.html b/src/app/payments/payments.component.html index 9b87aa444..aebab56d3 100644 --- a/src/app/payments/payments.component.html +++ b/src/app/payments/payments.component.html @@ -55,7 +55,7 @@ > Fail machines - + @if ((selected$ | async)?.length) { - - } @else { - - } + + + + + + + + + + +
+ + + Unit + + @for (unit of lifetimeUnits; track unit.value) { + {{ unit.label }} + } + + +
- @if (template) { - - } @else { - - } +
diff --git a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts index 0dc9366ac..a4c42f9b6 100644 --- a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts +++ b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts @@ -1,43 +1,49 @@ -import { - Subject, - catchError, - combineLatest, - debounceTime, - distinctUntilChanged, - map, - of, - shareReplay, - switchMap, - tap, -} from 'rxjs'; +import { EMPTY } from 'rxjs'; import { Clipboard } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, DestroyRef, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, form, min } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; import { InvoiceTemplateCreateParams } from '@vality/domain-proto/api_extensions'; +import { LifetimeInterval } from '@vality/domain-proto/domain'; import { DialogModule, DialogSuperclass, InputFieldModule, NotifyLogService, - getValueChanges, - progressTo, + observableResource, } from '@vality/matez'; import { DomainObjectsStoreService } from '~/api/domain-config'; import { ThriftInvoiceTemplatingService } from '~/api/services'; -import { ConfigService } from '~/services'; +import { MerchantFieldModule } from '../merchant-field'; +import { ShopFieldModule } from '../shop-field'; import { DomainMetadataFormExtensionsService, DomainThriftFormComponent } from '../thrift-api-crud'; +interface PaymentLinkParams extends Pick< + InvoiceTemplateCreateParams, + 'party_id' | 'shop_id' | 'details' +> { + lifetime: number; + lifetimeUnit: keyof LifetimeInterval; + name: string; + description: string; + email: string; + redirectUrl: string; + cancelUrl: string; + locale: string; +} + @Component({ templateUrl: './create-invoice-template-dialog.component.html', changeDetection: ChangeDetectionStrategy.Eager, @@ -50,125 +56,75 @@ import { DomainMetadataFormExtensionsService, DomainThriftFormComponent } from ' MatFormFieldModule, MatInputModule, MatIconModule, + MatSelectModule, InputFieldModule, MatDividerModule, + FormField, + ShopFieldModule, + MerchantFieldModule, ], }) export class CreateInvoiceTemplateDialogComponent extends DialogSuperclass { private invoiceTemplatingService = inject(ThriftInvoiceTemplatingService); private log = inject(NotifyLogService); - private dr = inject(DestroyRef); - private createTemplate$ = new Subject(); - private configService = inject(ConfigService); private clipboard = inject(Clipboard); - private fb = inject(FormBuilder); + private dr = inject(DestroyRef); private domainMetadataFormExtensionsService = inject(DomainMetadataFormExtensionsService); private domainStoreService = inject(DomainObjectsStoreService); - linkForm = this.fb.group({ - name: null, - description: null, - email: null, - redirectUrl: null, - cancelUrl: null, - locale: null, - }); - template$ = this.createTemplate$.pipe( - switchMap(() => - this.invoiceTemplatingService.Create(this.control.value).pipe( - progressTo(this.progress), - tap(() => { - this.log.success('Invoice template created successfully'); - }), - catchError((err) => { - this.log.error('Failed to create invoice template', err); - return of(null); - }), - ), - ), - takeUntilDestroyed(this.dr), - shareReplay(1), - ); - control = new FormControl( - { context: { type: 'application/json', data: '{}' } } as InvoiceTemplateCreateParams, - { nonNullable: true }, - ); - progress = signal(0); - link$ = combineLatest([ - this.template$, - this.configService.config.value$, - getValueChanges(this.linkForm).pipe(debounceTime(1000)), - ]).pipe( - map(([template, config, linkValues]) => { - const url = new URL( - `http${(config.checkout.https ?? true) ? 's' : ''}://${config.checkout.hostname}${config.checkout.path ?? '/v1/checkout.html'}`, - ); - url.searchParams.set('invoiceTemplateID', template.invoice_template.id); - url.searchParams.set( - 'invoiceTemplateAccessToken', - template.invoice_template_access_token.payload, - ); - const nonEmptyLinkValues = Object.entries(linkValues).filter(([, v]) => !!v); - for (const [key, value] of nonEmptyLinkValues) { - url.searchParams.set(key, value as string); - } - return url.toString(); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - extensions$ = combineLatest([ - this.domainMetadataFormExtensionsService.createFullDomainObjectsOptionsByType( - 'ShopConfigObject', - 'shop_config', - getValueChanges(this.control).pipe( - map((value) => value?.party_id?.id), - distinctUntilChanged(), - map((partyId) => - partyId - ? (obj) => obj.object.shop_config.data.party_ref.id === partyId - : () => true, - ), - ), - ), - this.domainMetadataFormExtensionsService.createFullDomainObjectsOptionsByType( - 'CurrencyObject', - 'currency', - getValueChanges(this.control).pipe( - map((value) => value?.shop_id?.id), - distinctUntilChanged(), - switchMap((shopId) => - shopId - ? this.domainStoreService.getObject({ - shop_config: { id: shopId }, - }).value$ - : of(null), - ), - map((shop) => - shop - ? (obj) => - obj.object.currency.data.symbolic_code === - shop.object.shop_config.data.account.currency.symbolic_code - : () => true, - ), - ), - ), - ]).pipe( - map((extensionGroups) => extensionGroups.flat()), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - create() { - this.createTemplate$.next(null); - } + lifetimeUnits: { value: keyof LifetimeInterval; label: string }[] = [ + { value: 'seconds', label: 'Seconds' }, + { value: 'minutes', label: 'Minutes' }, + { value: 'hours', label: 'Hours' }, + { value: 'days', label: 'Days' }, + { value: 'months', label: 'Months' }, + { value: 'years', label: 'Years' }, + ]; + controlModel = signal({ + lifetime: 30, + lifetimeUnit: 'days', + name: '', + description: '', + email: '', + redirectUrl: '', + cancelUrl: '', + locale: '', - confirm() { - this.closeWithSuccess(); - } + party_id: null, + shop_id: null, + details: null, + }); + control = form(this.controlModel, (path) => { + min(path.lifetime, 1, { message: 'Lifetime must be at least 1' }); + }); + invoiceTemplate = observableResource({ + params: EMPTY, + loader: ({ + shop_id, + party_id, + details, + lifetime, + lifetimeUnit, + ...params + }: PaymentLinkParams) => + this.invoiceTemplatingService.Create({ + shop_id, + party_id, + details, + invoice_lifetime: { [lifetimeUnit]: lifetime }, + url_params: new Map(Object.entries(params)), + context: { type: 'application/json', data: '{}' }, + }), + }); - copyLink() { - this.link$.pipe(takeUntilDestroyed(this.dr)).subscribe((link) => { - this.clipboard.copy(link); + onInit() { + this.invoiceTemplate.value$.pipe(takeUntilDestroyed(this.dr)).subscribe((result) => { + this.clipboard.copy(result.invoice_template_url.url); this.log.success('Link copied to clipboard'); }); } + + createAndCopyLink() { + this.invoiceTemplate.setParams(this.controlModel()); + } } From 611ed1db7cd246726df94cf7093c15e3b3cf92b5 Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:44:47 +0800 Subject: [PATCH 03/10] refactor: consolidate shop/merchant and duration inputs into reusable components in CreateInvoiceTemplateDialog --- .../select-field/select-field.component.html | 6 +- ...ate-invoice-template-dialog.component.html | 43 ++++----- ...reate-invoice-template-dialog.component.ts | 87 +++++++------------ .../duration-field.component.html | 13 +++ .../duration-field.component.ts | 44 ++++++++++ src/components/duration-field/index.ts | 1 + .../shop-field/shop-field.component.ts | 41 +++++++-- src/components/shop-merchant-field/index.ts | 1 + .../shop-merchant-field.component.html | 4 + .../shop-merchant-field.component.ts | 53 +++++++++++ 10 files changed, 201 insertions(+), 92 deletions(-) create mode 100644 src/components/duration-field/duration-field.component.html create mode 100644 src/components/duration-field/duration-field.component.ts create mode 100644 src/components/duration-field/index.ts create mode 100644 src/components/shop-merchant-field/index.ts create mode 100644 src/components/shop-merchant-field/shop-merchant-field.component.html create mode 100644 src/components/shop-merchant-field/shop-merchant-field.component.ts diff --git a/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.html b/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.html index 101eec25b..7c28bbd01 100644 --- a/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.html +++ b/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.html @@ -5,7 +5,11 @@ #select [closeOnSelect]="!multiple" [formControl]="control" - [items]="options" + [items]=" + options?.length || !control?.value + ? options + : [{ value: control.value, label: control.value }] + " [loading]="progress" [markFirst]="true" [multiple]="multiple" diff --git a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.html b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.html index 77bd9397c..896818f27 100644 --- a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.html +++ b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.html @@ -1,32 +1,19 @@ - - - - - - - - - -
- - - Unit - - @for (unit of lifetimeUnits; track unit.value) { - {{ unit.label }} - } - - +
+ + + + + + + + +
diff --git a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts index 91fa5d5d6..2a9d630c4 100644 --- a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts +++ b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts @@ -1,4 +1,5 @@ -import { EMPTY } from 'rxjs'; +import { EMPTY, of } from 'rxjs'; +import { distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators'; import { Clipboard } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; @@ -10,7 +11,7 @@ import { inject, signal, } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { ReactiveFormsModule } from '@angular/forms'; import { FormField, form } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; @@ -28,8 +29,8 @@ import { DomainObjectsStoreService } from '~/api/domain-config'; import { ThriftInvoiceTemplatingService } from '~/api/services'; import { Duration, DurationFieldComponent } from '../duration-field'; +import { InvoiceTemplateDetailsFieldComponent } from '../invoice-template-details-field'; import { PartyShop, ShopMerchantFieldComponent } from '../shop-merchant-field'; -import { DomainThriftFormComponent } from '../thrift-api-crud'; interface PaymentLinkParams extends Pick { partyShop: PartyShop; @@ -48,11 +49,11 @@ interface PaymentLinkParams extends Pick imports: [ CommonModule, DialogModule, - DomainThriftFormComponent, ReactiveFormsModule, InputFieldModule, FormField, DurationFieldComponent, + InvoiceTemplateDetailsFieldComponent, ShopMerchantFieldComponent, MatButtonModule, ], @@ -83,6 +84,20 @@ export class CreateInvoiceTemplateDialogComponent details: null, }); control = form(this.controlModel); + currency$ = toObservable(this.controlModel).pipe( + map(({ partyShop }) => partyShop.shop_id), + distinctUntilChanged(), + switchMap((shopId) => + shopId + ? this.domainStoreService.getObject({ shop_config: { id: shopId } }).value$ + : of(null), + ), + map( + (shop) => + shop?.object?.shop_config?.data?.account?.currency?.symbolic_code ?? undefined, + ), + shareReplay({ bufferSize: 1, refCount: true }), + ); invoiceTemplate = observableResource({ params: EMPTY, loader: ({ partyShop, details, lifetime, ...params }: PaymentLinkParams) => diff --git a/src/components/duration-field/duration-field.component.html b/src/components/duration-field/duration-field.component.html index 084227fa3..c89364509 100644 --- a/src/components/duration-field/duration-field.component.html +++ b/src/components/duration-field/duration-field.component.html @@ -9,5 +9,6 @@ [formField]="control.unit" [options]="unitOptions" class="flex-1" + label="Unit" > diff --git a/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html new file mode 100644 index 000000000..3ae194539 --- /dev/null +++ b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html @@ -0,0 +1,11 @@ +
+ +
+ + +
+
diff --git a/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts new file mode 100644 index 000000000..eb1138492 --- /dev/null +++ b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts @@ -0,0 +1,46 @@ +import { Component, DestroyRef, effect, inject, input, model } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { FormField, FormValueControl, form } from '@angular/forms/signals'; + +import { Cash, InvoiceLine } from '@vality/domain-proto/domain'; +import { InputFieldModule } from '@vality/matez'; + +import { + CashFieldComponent, + Cash as CashFieldValue, +} from '~/components/cash-field/cash-field.component'; + +@Component({ + selector: 'cc-invoice-line-field', + templateUrl: './invoice-line-field.component.html', + imports: [ReactiveFormsModule, FormField, InputFieldModule, CashFieldComponent], +}) +export class InvoiceLineFieldComponent implements FormValueControl { + private destroyRef = inject(DestroyRef); + + currency = input(); + value = model(null); + control = form(this.value); + priceControl = new FormControl(null); + + constructor() { + this.priceControl.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((price) => { + this.value.update((value) => ({ + ...value, + price: this.toCash(price), + })); + }); + } + + private toCash(value: CashFieldValue): Cash { + return value + ? { + amount: value.amount, + currency: { symbolic_code: this.currency() || value.currencyCode }, + } + : null; + } +} diff --git a/src/components/invoice-template-details-field/index.ts b/src/components/invoice-template-details-field/index.ts new file mode 100644 index 000000000..380467e53 --- /dev/null +++ b/src/components/invoice-template-details-field/index.ts @@ -0,0 +1 @@ +export * from './invoice-template-details-field.component'; diff --git a/src/components/invoice-template-details-field/invoice-template-details-field.component.html b/src/components/invoice-template-details-field/invoice-template-details-field.component.html new file mode 100644 index 000000000..4de7eb7fc --- /dev/null +++ b/src/components/invoice-template-details-field/invoice-template-details-field.component.html @@ -0,0 +1,48 @@ +
+ + Product + Cart + + + @if (detailsType() === 'cart') { + + @for (line of formValue()?.cart?.lines ?? []; track $index) { + + + Product #{{ $index + 1 }} + + + + + } + + + } @else { + + } +
diff --git a/src/components/invoice-template-details-field/invoice-template-details-field.component.ts b/src/components/invoice-template-details-field/invoice-template-details-field.component.ts new file mode 100644 index 000000000..8471ce897 --- /dev/null +++ b/src/components/invoice-template-details-field/invoice-template-details-field.component.ts @@ -0,0 +1,125 @@ +import { Overwrite } from 'utility-types'; + +import { Component, computed, input, model } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, FormValueControl, form, transformedValue } from '@angular/forms/signals'; +import { MatButtonModule } from '@angular/material/button'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatIconModule } from '@angular/material/icon'; +import { MatRadioModule } from '@angular/material/radio'; + +import { Cash, InvoiceLine, InvoiceTemplateDetails } from '@vality/domain-proto/domain'; + +import { InvoiceLineFieldComponent } from './components/invoice-line-field/invoice-line-field.component'; + +type InvoiceTemplateDetailsFormValue = Overwrite; + +@Component({ + selector: 'cc-invoice-template-details-field', + templateUrl: './invoice-template-details-field.component.html', + imports: [ + ReactiveFormsModule, + FormField, + InvoiceLineFieldComponent, + MatButtonModule, + MatExpansionModule, + MatIconModule, + MatRadioModule, + ], +}) +export class InvoiceTemplateDetailsFieldComponent implements FormValueControl { + currency = input(); + value = model(null); + formValue = transformedValue( + this.value, + { + parse: (value) => ({ value: this.toInvoiceTemplateDetails(value) }), + format: (value) => this.toFormValue(value), + }, + ); + control = form(this.formValue); + detailsType = computed<'cart' | 'product'>(() => (this.formValue()?.cart ? 'cart' : 'product')); + + selectDetailsType(type: 'cart' | 'product') { + if (type !== this.detailsType()) { + this.formValue.set( + type === 'cart' + ? { cart: { lines: [this.createInvoiceLine()] } } + : { product: this.createInvoiceLine() }, + ); + } + } + + addInvoiceLine() { + this.formValue.update((value) => ({ + cart: { + lines: [...(value?.cart?.lines ?? []), this.createInvoiceLine()], + }, + })); + } + + removeInvoiceLine(index: number) { + this.formValue.update((value) => ({ + cart: { + lines: + (value?.cart?.lines.length ?? 0) > 1 + ? value.cart.lines.filter((_, i) => i !== index) + : value.cart.lines, + }, + })); + } + + private createCash(): Cash { + return { + amount: null, + currency: { symbolic_code: this.currency() ?? '' }, + }; + } + + private createInvoiceLine(): InvoiceLine { + return { + product: '', + quantity: 1, + price: this.createCash(), + metadata: new Map(), + }; + } + + private toInvoiceTemplateDetails( + value: InvoiceTemplateDetailsFormValue, + ): InvoiceTemplateDetails { + if (value?.product) { + const { product, price, metadata } = value.product; + return { + product: { + product, + price: { fixed: price }, + metadata, + }, + }; + } + return value?.cart ? { cart: value.cart } : null; + } + + private toFormValue(value: InvoiceTemplateDetails): InvoiceTemplateDetailsFormValue { + if (value?.product) { + return { + product: { + product: value.product.product, + quantity: 1, + price: value.product.price.fixed ?? this.createCash(), + metadata: value.product.metadata, + }, + }; + } + return value?.cart + ? { + cart: { + lines: value.cart.lines.length + ? value.cart.lines + : [this.createInvoiceLine()], + }, + } + : null; + } +} From dc9a0cbe7bf62a36089cd7e8f50e99fb2c26ad89 Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:06:10 +0800 Subject: [PATCH 05/10] chore: update @vality/fistful-proto version to 2.0.1-441d9bc --- package-lock.json | 6 ++++-- package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c90320afb..163968d4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@sentry/angular": "^10.68.0", "@tailwindcss/postcss": "^4.3.1", "@vality/domain-proto": "^2.0.2-99c91c9.0", - "@vality/fistful-proto": "^2.0.1-9864622.0", + "@vality/fistful-proto": "^2.0.1-441d9bc.0", "@vality/machinegun-proto": "^1.0.1-273f2f3.0", "@vality/magista-proto": "^2.0.2-bd58cea.0", "@vality/repairer-proto": "^2.0.2-07b2a51.0", @@ -5728,7 +5728,9 @@ "license": "Apache-2.0" }, "node_modules/@vality/fistful-proto": { - "version": "2.0.1-9864622.0", + "version": "2.0.1-441d9bc.0", + "resolved": "https://registry.npmjs.org/@vality/fistful-proto/-/fistful-proto-2.0.1-441d9bc.0.tgz", + "integrity": "sha512-d03yAxK0DKM0K55vPXz24kMep/ZyrMJFVh4ndHIYpl+eneAOuasvqMpKccaP/3+CBrmWZK8IgJ1hWePQtH/mBw==", "license": "Apache-2.0" }, "node_modules/@vality/machinegun-proto": { diff --git a/package.json b/package.json index 7f1ffad61..e7ae91a56 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@sentry/angular": "^10.68.0", "@tailwindcss/postcss": "^4.3.1", "@vality/domain-proto": "^2.0.2-99c91c9.0", - "@vality/fistful-proto": "^2.0.1-9864622.0", + "@vality/fistful-proto": "^2.0.1-441d9bc.0", "@vality/machinegun-proto": "^1.0.1-273f2f3.0", "@vality/magista-proto": "^2.0.2-bd58cea.0", "@vality/repairer-proto": "^2.0.2-07b2a51.0", From 4fd33fcf6cfa191b429936ad29770a0fe4fa555a Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:17:18 +0800 Subject: [PATCH 06/10] feat: add support for changed amount in currency column creation --- src/app/withdrawals/withdrawals.component.ts | 6 ++++++ src/utils/table/create-currency-column.ts | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/withdrawals/withdrawals.component.ts b/src/app/withdrawals/withdrawals.component.ts index bc7a7f463..2a8549752 100644 --- a/src/app/withdrawals/withdrawals.component.ts +++ b/src/app/withdrawals/withdrawals.component.ts @@ -101,6 +101,12 @@ export class WithdrawalsComponent implements OnInit { createCurrencyColumn((d) => ({ amount: d.fee, code: d.currency_symbolic_code }), { field: 'fee', }), + createCurrencyColumn( + (d) => ({ amount: d.changed_amount, code: d.changed_currency_symbolic_code }), + { + field: 'changed', + }, + ), { field: 'status', cell: (d) => ({ diff --git a/src/utils/table/create-currency-column.ts b/src/utils/table/create-currency-column.ts index 1f4d6a31b..2de7dc542 100644 --- a/src/utils/table/create-currency-column.ts +++ b/src/utils/table/create-currency-column.ts @@ -1,4 +1,4 @@ -import { groupBy, uniq } from 'lodash-es'; +import { groupBy, isNil, uniq } from 'lodash-es'; import { combineLatest, of } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; @@ -38,7 +38,7 @@ export const createCurrencyColumn = createColumn( (currencyValue: CurrencyValue | { values: CurrencyValue[]; isSum?: boolean }) => { const isSum = 'isSum' in currencyValue ? currencyValue.isSum : false; const currencyValues = ('values' in currencyValue ? currencyValue.values : [currencyValue]) - .filter(Boolean) + .filter((v) => !isNil(v?.amount) && !isNil(v?.code)) .sort((a, b) => b.amount - a.amount); if (!currencyValues?.length) { return of(undefined); From 09518b350426a50eb870a196c974c03fe4ec8e5f Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:20:26 +0800 Subject: [PATCH 07/10] refactor: replace console.info with console.dir for improved logging output --- src/utils/thrift/provide-thrift-services.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/utils/thrift/provide-thrift-services.ts b/src/utils/thrift/provide-thrift-services.ts index a27aeeaca..6bed684f2 100644 --- a/src/utils/thrift/provide-thrift-services.ts +++ b/src/utils/thrift/provide-thrift-services.ts @@ -7,7 +7,6 @@ import { Type, inject, isDevMode, makeEnvironmentProviders } from '@angular/core import * as Sentry from '@sentry/angular'; import { ConnectOptions } from '@vality/domain-proto'; -import { toJson } from '@vality/ng-thrift'; import { ConfigService, KeycloakUserService } from '~/services'; @@ -117,9 +116,9 @@ const logger: ConnectOptions['loggingFn'] = (params) => { console.error(parsedError.error); if (LOGGING.fullLogging) { console.log('Arguments'); - console.log(JSON.stringify(toJson(params.args), null, 2)); + console.dir(params.args); console.log('Headers'); - console.log(params.headers); + console.dir(params.headers); } console.groupEnd(); return; @@ -128,11 +127,11 @@ const logger: ConnectOptions['loggingFn'] = (params) => { if (LOGGING.fullLogging) { console.groupCollapsed(`🟢\u00A0${info}`); console.log('Arguments'); - console.log(JSON.stringify(toJson(params.args), null, 2)); + console.dir(params.args); console.log('Response'); - console.log(JSON.stringify(toJson(params.response), null, 2)); + console.dir(params.response); console.log('Headers'); - console.log(params.headers); + console.dir(params.headers); console.groupEnd(); } return; From ee872cb13ce7232d490c66686676e16a34d0d373 Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:43:19 +0800 Subject: [PATCH 08/10] refactor: enhance logging output by consolidating arguments, headers, and responses into a single structured log --- src/utils/thrift/provide-thrift-services.ts | 31 ++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/utils/thrift/provide-thrift-services.ts b/src/utils/thrift/provide-thrift-services.ts index 6bed684f2..d074f34d2 100644 --- a/src/utils/thrift/provide-thrift-services.ts +++ b/src/utils/thrift/provide-thrift-services.ts @@ -115,10 +115,16 @@ const logger: ConnectOptions['loggingFn'] = (params) => { ); console.error(parsedError.error); if (LOGGING.fullLogging) { - console.log('Arguments'); - console.dir(params.args); - console.log('Headers'); - console.dir(params.headers); + console.dir( + { + Arguments: params.args, + Headers: params.headers, + }, + { + depth: 4, + compact: false, + }, + ); } console.groupEnd(); return; @@ -126,12 +132,17 @@ const logger: ConnectOptions['loggingFn'] = (params) => { case 'success': { if (LOGGING.fullLogging) { console.groupCollapsed(`🟢\u00A0${info}`); - console.log('Arguments'); - console.dir(params.args); - console.log('Response'); - console.dir(params.response); - console.log('Headers'); - console.dir(params.headers); + console.dir( + { + Arguments: params.args, + Response: params.response, + Headers: params.headers, + }, + { + depth: 4, + compact: false, + }, + ); console.groupEnd(); } return; From 9136b44d577dd3587c6b58629bb8b7bc28c63de1 Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:17:15 +0800 Subject: [PATCH 09/10] feat: update addInvoiceLine method to include product line creation --- .../cash-field/cash-field.component.html | 18 -- .../cash-field/cash-field.component.ts | 181 ------------------ src/components/cash-field/index.ts | 1 - .../invoice-line-field.component.html | 6 +- .../invoice-line-field.component.ts | 79 +++++--- ...nvoice-template-details-field.component.ts | 2 +- 6 files changed, 63 insertions(+), 224 deletions(-) delete mode 100644 src/components/cash-field/cash-field.component.html delete mode 100644 src/components/cash-field/cash-field.component.ts delete mode 100644 src/components/cash-field/index.ts diff --git a/src/components/cash-field/cash-field.component.html b/src/components/cash-field/cash-field.component.html deleted file mode 100644 index a854a0829..000000000 --- a/src/components/cash-field/cash-field.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
- - {{ label || 'Amount' }} - {{ prefix }}  - - - -
diff --git a/src/components/cash-field/cash-field.component.ts b/src/components/cash-field/cash-field.component.ts deleted file mode 100644 index dd75bf277..000000000 --- a/src/components/cash-field/cash-field.component.ts +++ /dev/null @@ -1,181 +0,0 @@ -import isNil from 'lodash-es/isNil'; -import { combineLatest } from 'rxjs'; -import { distinctUntilChanged, map, shareReplay, startWith, take } from 'rxjs/operators'; - -import { CommonModule, getCurrencySymbol } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - DestroyRef, - Input, - LOCALE_ID, - OnInit, - booleanAttribute, - inject, -} from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormControl, ReactiveFormsModule, ValidationErrors, Validator } from '@angular/forms'; -import { MatFormField } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { InputMaskModule, createMask } from '@ngneat/input-mask'; - -import { CurrencyObject } from '@vality/domain-proto/domain'; -import { - FormComponentSuperclass, - Option, - SelectFieldModule, - compareDifferentTypes, - createControlProviders, - getValueChanges, - toMajorByExponent, - toMinorByExponent, -} from '@vality/matez'; - -import { CurrenciesStoreService } from '~/api/domain-config'; - -export interface Cash { - amount: number; - currencyCode: string; -} - -const GROUP_SEPARATOR = ' '; -const DEFAULT_EXPONENT = 2; -const RADIX_POINT = '.'; - -@Component({ - selector: 'cc-cash-field', - templateUrl: './cash-field.component.html', - providers: createControlProviders(() => CashFieldComponent), - changeDetection: ChangeDetectionStrategy.Eager, - imports: [ - MatFormField, - ReactiveFormsModule, - InputMaskModule, - SelectFieldModule, - CommonModule, - MatInputModule, - ], -}) -export class CashFieldComponent extends FormComponentSuperclass implements Validator, OnInit { - private _locale = inject(LOCALE_ID); - private destroyRef = inject(DestroyRef); - private currenciesStoreService = inject(CurrenciesStoreService); - - @Input() label?: string; - @Input({ transform: booleanAttribute }) required: boolean = false; - - amountControl = new FormControl(null); - currencyControl = new FormControl(null); - - options$ = this.currenciesStoreService.currencies$.pipe( - startWith([] as CurrencyObject[]), - map((objs): Option[] => - objs - .sort((a, b) => compareDifferentTypes(a.data.symbolic_code, b.data.symbolic_code)) - .map((s) => ({ - label: s.data.symbolic_code, - description: s.data.name, - value: s, - })), - ), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - currencyExponent$ = getValueChanges(this.currencyControl).pipe( - map((obj) => obj?.data?.exponent ?? DEFAULT_EXPONENT), - distinctUntilChanged(), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - amountMask$ = this.currencyExponent$.pipe( - distinctUntilChanged(), - map((exponent) => - createMask({ - alias: 'numeric', - groupSeparator: GROUP_SEPARATOR, - digits: exponent, - digitsOptional: true, - placeholder: '', - onBeforePaste: (pastedValue: string) => - this.convertPastedToStringNumber(pastedValue), - }), - ), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - get currencyCode() { - return this.currencyControl.value?.data?.symbolic_code; - } - - get prefix() { - return getCurrencySymbol(this.currencyCode, 'narrow', this._locale); - } - - override ngOnInit() { - super.ngOnInit(); - combineLatest([ - combineLatest([getValueChanges(this.amountControl), this.currencyExponent$]).pipe( - map(([amountStr, exponent]) => { - const amount = amountStr - ? Number(amountStr.replaceAll(GROUP_SEPARATOR, '')) - : null; - return isNil(amount) ? null : toMinorByExponent(amount, exponent); - }), - distinctUntilChanged(), - ), - getValueChanges(this.currencyControl).pipe( - map((obj) => obj?.data?.symbolic_code), - distinctUntilChanged(), - ), - ]) - .pipe( - map(([amount, currencyCode]) => - !isNil(amount) && currencyCode ? { amount, currencyCode } : null, - ), - distinctUntilChanged(), - takeUntilDestroyed(this.destroyRef), - ) - .subscribe((value) => { - this.emitOutgoingValue(value); - }); - } - - override validate(): ValidationErrors | null { - return !this.amountControl.value || !this.currencyControl.value - ? { invalidCash: true } - : null; - } - - handleIncomingValue(value: Cash) { - const { currencyCode, amount } = value || {}; - if (!currencyCode) { - this.setValues(amount, null); - } - this.options$ - .pipe( - map( - (options) => - options.find((o) => o.value?.data?.symbolic_code === value.currencyCode) - ?.value ?? null, - ), - take(1), - takeUntilDestroyed(this.destroyRef), - ) - .subscribe((obj) => { - this.setValues(amount, obj); - }); - } - - private setValues(amount: number, currencyObject: CurrencyObject) { - this.currencyControl.setValue(currencyObject); - this.amountControl.setValue( - typeof amount === 'number' - ? String( - toMajorByExponent(amount, currencyObject?.data?.exponent ?? DEFAULT_EXPONENT), - ) - : null, - ); - } - - private convertPastedToStringNumber(pastedValue: string) { - return pastedValue.replaceAll(',', RADIX_POINT); - } -} diff --git a/src/components/cash-field/index.ts b/src/components/cash-field/index.ts deleted file mode 100644 index e4923d100..000000000 --- a/src/components/cash-field/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './cash-field.component'; diff --git a/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html index 3ae194539..19c2fb946 100644 --- a/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html +++ b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html @@ -6,6 +6,10 @@ label="Quantity" type="number" > - + diff --git a/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts index eb1138492..2d14d46e1 100644 --- a/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts +++ b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts @@ -1,45 +1,80 @@ -import { Component, DestroyRef, effect, inject, input, model } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; -import { FormField, FormValueControl, form } from '@angular/forms/signals'; +import { Overwrite } from 'utility-types'; + +import { Component, effect, input, model, untracked } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, FormValueControl, form, transformedValue } from '@angular/forms/signals'; import { Cash, InvoiceLine } from '@vality/domain-proto/domain'; import { InputFieldModule } from '@vality/matez'; import { - CashFieldComponent, - Cash as CashFieldValue, -} from '~/components/cash-field/cash-field.component'; + SourceCash, + SourceCashFieldComponent, +} from '~/components/source-cash-field/source-cash-field.component'; + +type InvoiceLineFormValue = Overwrite; @Component({ selector: 'cc-invoice-line-field', templateUrl: './invoice-line-field.component.html', - imports: [ReactiveFormsModule, FormField, InputFieldModule, CashFieldComponent], + imports: [ReactiveFormsModule, FormField, InputFieldModule, SourceCashFieldComponent], }) export class InvoiceLineFieldComponent implements FormValueControl { - private destroyRef = inject(DestroyRef); - currency = input(); value = model(null); - control = form(this.value); - priceControl = new FormControl(null); + formValue = transformedValue(this.value, { + parse: (value) => ({ value: this.toInvoiceLine(value) }), + format: (value) => this.toFormValue(value), + }); + control = form(this.formValue); constructor() { - this.priceControl.valueChanges - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((price) => { - this.value.update((value) => ({ - ...value, - price: this.toCash(price), - })); - }); + effect(() => { + const currencySymbolicCode = this.currency(); + if (currencySymbolicCode) { + untracked(() => { + this.formValue.update((value) => ({ + ...value, + price: { + ...value.price, + sourceId: null, + currencySymbolicCode, + }, + })); + }); + } + }); + } + + private toInvoiceLine(value: InvoiceLineFormValue): InvoiceLine { + return value + ? { + ...value, + price: this.toCash(value.price), + } + : null; + } + + private toFormValue(value: InvoiceLine): InvoiceLineFormValue { + return value + ? { + ...value, + price: { + amount: value.price?.amount, + sourceId: null, + currencySymbolicCode: this.currency() || value.price?.currency?.symbolic_code, + }, + } + : null; } - private toCash(value: CashFieldValue): Cash { + private toCash(value: SourceCash): Cash { return value ? { amount: value.amount, - currency: { symbolic_code: this.currency() || value.currencyCode }, + currency: { + symbolic_code: this.currency() || value.currencySymbolicCode, + }, } : null; } diff --git a/src/components/invoice-template-details-field/invoice-template-details-field.component.ts b/src/components/invoice-template-details-field/invoice-template-details-field.component.ts index 8471ce897..3cc2a72ac 100644 --- a/src/components/invoice-template-details-field/invoice-template-details-field.component.ts +++ b/src/components/invoice-template-details-field/invoice-template-details-field.component.ts @@ -120,6 +120,6 @@ export class InvoiceTemplateDetailsFieldComponent implements FormValueControl Date: Mon, 10 Aug 2026 17:58:59 +0800 Subject: [PATCH 10/10] feat: add payment link URL display and copy functionality in invoice template dialog --- ...ate-invoice-template-dialog.component.html | 10 +++ ...reate-invoice-template-dialog.component.ts | 40 +++++++--- .../shop-field/shop-field.component.html | 8 +- .../shop-field/shop-field.component.ts | 76 +++++++++---------- .../shop-merchant-field.component.html | 2 +- .../source-cash-field.component.ts | 4 +- src/utils/thrift/provide-thrift-services.ts | 8 +- 7 files changed, 88 insertions(+), 60 deletions(-) diff --git a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.html b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.html index ab4b2a954..b36469970 100644 --- a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.html +++ b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.html @@ -12,6 +12,16 @@ [formField]="control.details" > + + + URL + + content_copy +