From a4f44dbb58918760c5076dad789e73ba377f2a4d Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 23:56:59 +0400 Subject: [PATCH] feat: Phase 7 frontend - Refund/Reconciliation core + Payments & Finance section New core/finance (Refund/ReconciliationRecord/Settlement models + gateway/ token, mock backed, seeded empty) and features/admin/finance (facade + page: reconciliation queue with resolve action, settlements placeholder). New /backoffice/finance route + nav entry, nav i18n key in all 3 languages. Against docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md. Note: does not wire AdminOrdersLocalGateway's existing mock requestRefund(id) method into this new Refund flow yet - that's a small follow-up once Phase 2's real Order backend exists to refund against. Co-Authored-By: Claude Sonnet 5 --- src/app/app.routes.ts | 9 +++++ .../finance/models/reconciliation.model.ts | 38 +++++++++++++++++++ .../services/finance-gateway.interface.ts | 10 +++++ .../finance/services/finance-gateway.token.ts | 9 +++++ .../finance/services/finance-local.gateway.ts | 32 ++++++++++++++++ .../finance/facade/admin-finance.facade.ts | 26 +++++++++++++ .../pages/admin-finance-page.component.html | 29 ++++++++++++++ .../pages/admin-finance-page.component.scss | 29 ++++++++++++++ .../pages/admin-finance-page.component.ts | 21 ++++++++++ .../features/admin/shell/admin-nav.model.ts | 1 + src/app/i18n/en.ts | 1 + src/app/i18n/hy.ts | 1 + src/app/i18n/ru.ts | 1 + src/app/i18n/translations.ts | 1 + 14 files changed, 208 insertions(+) create mode 100644 src/app/core/finance/models/reconciliation.model.ts create mode 100644 src/app/core/finance/services/finance-gateway.interface.ts create mode 100644 src/app/core/finance/services/finance-gateway.token.ts create mode 100644 src/app/core/finance/services/finance-local.gateway.ts create mode 100644 src/app/features/admin/finance/facade/admin-finance.facade.ts create mode 100644 src/app/features/admin/finance/pages/admin-finance-page.component.html create mode 100644 src/app/features/admin/finance/pages/admin-finance-page.component.scss create mode 100644 src/app/features/admin/finance/pages/admin-finance-page.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 85e23b0..eda6bfb 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -209,6 +209,15 @@ const coreRoutes: Routes = [ breadcrumb: [{ labelKey: 'adminShell.nav.integrations' }] } }, + { + path: 'finance', + loadComponent: () => import('./features/admin/finance/pages/admin-finance-page.component').then(m => m.AdminFinancePageComponent), + data: { + titleKey: 'adminShell.nav.finance', + descriptionKey: 'adminShell.nav.finance', + breadcrumb: [{ labelKey: 'adminShell.nav.finance' }] + } + }, { path: 'moderation', loadComponent: () => import('./features/admin/moderation/pages/admin-reviews-list-page.component').then(m => m.AdminReviewsListPageComponent), diff --git a/src/app/core/finance/models/reconciliation.model.ts b/src/app/core/finance/models/reconciliation.model.ts new file mode 100644 index 0000000..4ee0d93 --- /dev/null +++ b/src/app/core/finance/models/reconciliation.model.ts @@ -0,0 +1,38 @@ +import { Money } from '../../pricing/models/money.model'; + +/** Per docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md §1-3. */ +export interface Refund { + id: string; + orderId: string; + orderLineIds: string[]; + amount: Money; + reason: string; + actor: string; + status: 'requested' | 'approved' | 'processing' | 'completed' | 'failed'; + requestedAt: string; + completedAt?: string; +} + +export interface ReconciliationRecord { + id: string; + orderId: string; + providerPaymentId?: string; + internalAmount: Money; + providerAmount?: Money; + matchStrategy: 'provider_payment_id' | 'merchant_reference' | 'amount_currency_fallback'; + result: 'matched' | 'unmatched' | 'duplicate' | 'amount_mismatch' | 'status_mismatch'; + resolvedBy?: string; + resolvedAt?: string; +} + +export interface Settlement { + id: string; + sellerId: string; + periodStart: string; + periodEnd: string; + grossAmount: Money; + commission: Money; + refunds: Money; + netPayout: Money; + status: 'pending' | 'paid'; +} diff --git a/src/app/core/finance/services/finance-gateway.interface.ts b/src/app/core/finance/services/finance-gateway.interface.ts new file mode 100644 index 0000000..9cd8095 --- /dev/null +++ b/src/app/core/finance/services/finance-gateway.interface.ts @@ -0,0 +1,10 @@ +import { Observable } from 'rxjs'; +import { ReconciliationRecord, Refund, Settlement } from '../models/reconciliation.model'; + +/** Per docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md §1-3. */ +export interface FinanceGateway { + loadRefunds(orderId?: string): Observable; + loadReconciliationQueue(): Observable; + resolveReconciliation(id: string, note: string): Observable; + loadSettlements(sellerId?: string): Observable; +} diff --git a/src/app/core/finance/services/finance-gateway.token.ts b/src/app/core/finance/services/finance-gateway.token.ts new file mode 100644 index 0000000..33c32d8 --- /dev/null +++ b/src/app/core/finance/services/finance-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { FinanceGateway } from './finance-gateway.interface'; +import { FinanceLocalGateway } from './finance-local.gateway'; + +/** Swap point for docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md. */ +export const FINANCE_GATEWAY = new InjectionToken('FINANCE_GATEWAY', { + providedIn: 'root', + factory: () => inject(FinanceLocalGateway), +}); diff --git a/src/app/core/finance/services/finance-local.gateway.ts b/src/app/core/finance/services/finance-local.gateway.ts new file mode 100644 index 0000000..d19fcfe --- /dev/null +++ b/src/app/core/finance/services/finance-local.gateway.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { ReconciliationRecord, Refund, Settlement } from '../models/reconciliation.model'; +import { FinanceGateway } from './finance-gateway.interface'; + +/** + * Seeded empty. AdminOrdersLocalGateway's requestRefund(id) exists as a mock + * method but nothing reads it into a real Refund/reconciliation flow yet - + * this gateway is the real target once docs/backend/ + * PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md ships. + */ +@Injectable({ providedIn: 'root' }) +export class FinanceLocalGateway implements FinanceGateway { + private reconciliation: ReconciliationRecord[] = []; + + loadRefunds(_orderId?: string): Observable { + return of([]); + } + + loadReconciliationQueue(): Observable { + return of(this.reconciliation); + } + + resolveReconciliation(id: string, _note: string): Observable { + this.reconciliation = this.reconciliation.filter(r => r.id !== id); + return of(void 0); + } + + loadSettlements(_sellerId?: string): Observable { + return of([]); + } +} diff --git a/src/app/features/admin/finance/facade/admin-finance.facade.ts b/src/app/features/admin/finance/facade/admin-finance.facade.ts new file mode 100644 index 0000000..648bc13 --- /dev/null +++ b/src/app/features/admin/finance/facade/admin-finance.facade.ts @@ -0,0 +1,26 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { take } from 'rxjs/operators'; +import { ReconciliationRecord, Settlement } from '../../../../core/finance/models/reconciliation.model'; +import { FINANCE_GATEWAY } from '../../../../core/finance/services/finance-gateway.token'; + +@Injectable({ providedIn: 'root' }) +export class AdminFinanceFacade { + private readonly gateway = inject(FINANCE_GATEWAY); + + readonly reconciliation = signal([]); + readonly settlements = signal([]); + readonly loading = signal(false); + + load(): void { + this.loading.set(true); + this.gateway.loadReconciliationQueue().pipe(take(1)).subscribe(items => { + this.reconciliation.set(items); + this.loading.set(false); + }); + this.gateway.loadSettlements().pipe(take(1)).subscribe(items => this.settlements.set(items)); + } + + resolve(id: string, note: string): void { + this.gateway.resolveReconciliation(id, note).pipe(take(1)).subscribe(() => this.load()); + } +} diff --git a/src/app/features/admin/finance/pages/admin-finance-page.component.html b/src/app/features/admin/finance/pages/admin-finance-page.component.html new file mode 100644 index 0000000..3b2f31b --- /dev/null +++ b/src/app/features/admin/finance/pages/admin-finance-page.component.html @@ -0,0 +1,29 @@ +
+
+

Payments & Finance

+

Reconciliation queue and settlements. Mock-gateway backed until docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md ships.

+
+ +
+

Reconciliation queue

+ @if (facade.reconciliation().length === 0 && !facade.loading()) { + + } @else { +
    + @for (r of facade.reconciliation(); track r.id) { +
  • + {{ r.orderId }} - {{ r.result }} + Resolve +
  • + } +
+ } +
+ +
+

Settlements

+ @if (facade.settlements().length === 0) { + + } +
+
diff --git a/src/app/features/admin/finance/pages/admin-finance-page.component.scss b/src/app/features/admin/finance/pages/admin-finance-page.component.scss new file mode 100644 index 0000000..7943105 --- /dev/null +++ b/src/app/features/admin/finance/pages/admin-finance-page.component.scss @@ -0,0 +1,29 @@ +.finance-page { + display: flex; + flex-direction: column; + gap: 24px; + + section { + display: flex; + flex-direction: column; + gap: 12px; + } + + ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; + + li { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + } + } +} diff --git a/src/app/features/admin/finance/pages/admin-finance-page.component.ts b/src/app/features/admin/finance/pages/admin-finance-page.component.ts new file mode 100644 index 0000000..d631a0b --- /dev/null +++ b/src/app/features/admin/finance/pages/admin-finance-page.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AdminFinanceFacade } from '../facade/admin-finance.facade'; +import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-state.component'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; + +@Component({ + selector: 'app-admin-finance-page', + standalone: true, + imports: [CommonModule, EmptyStateComponent, ButtonComponent], + templateUrl: './admin-finance-page.component.html', + styleUrls: ['./admin-finance-page.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AdminFinancePageComponent { + readonly facade = inject(AdminFinanceFacade); + + constructor() { + this.facade.load(); + } +} diff --git a/src/app/features/admin/shell/admin-nav.model.ts b/src/app/features/admin/shell/admin-nav.model.ts index 57d0356..d0aa9a5 100644 --- a/src/app/features/admin/shell/admin-nav.model.ts +++ b/src/app/features/admin/shell/admin-nav.model.ts @@ -40,6 +40,7 @@ export const ADMIN_NAV_PRIMARY: AdminNavEntry[] = [ { type: 'link', id: 'moderation', icon: 'star', labelKey: 'adminShell.nav.moderation', path: ['moderation'] }, { type: 'link', id: 'notifications', icon: 'bell', labelKey: 'adminShell.nav.notifications', path: ['notifications'] }, { type: 'link', id: 'integrations', icon: 'network', labelKey: 'adminShell.nav.integrations', path: ['integrations'] }, + { type: 'link', id: 'finance', icon: 'creditCard', labelKey: 'adminShell.nav.finance', path: ['finance'] }, { type: 'link', id: 'reports', icon: 'chartBar', labelKey: 'adminShell.nav.reports', path: ['reports'] }, { type: 'group', labelKey: 'adminShell.nav.partnersGroup' }, { type: 'link', id: 'seller-management', icon: 'store', labelKey: 'adminShell.nav.sellerManagement', path: ['partners', 'seller-management'] }, diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index 5f35224..b08f38e 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -2061,6 +2061,7 @@ export const en: Translations = { moderation: 'Reviews & Moderation', notifications: 'Notifications', integrations: 'Integrations', + finance: 'Payments & Finance', reviews: 'Reviews', reports: 'Reports', partnersGroup: 'Partners', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 85b7e7b..39e987a 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -2055,6 +2055,7 @@ export const hy: Translations = { moderation: 'Կարծիքներ և մոդերացիա', notifications: 'Ծանուցումներ', integrations: 'Ինտեգրումներ', + finance: 'Վճարումներ և ֆինանսներ', transactions: 'Գործարքներ', reviews: 'Կարծիքներ', reports: 'Հաշվետվություններ', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index a2d30f1..b0e3afe 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -2055,6 +2055,7 @@ export const ru: Translations = { moderation: 'Отзывы и модерация', notifications: 'Уведомления', integrations: 'Интеграции', + finance: 'Платежи и финансы', transactions: 'Транзакции', reviews: 'Отзывы', reports: 'Отчёты', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index d5cbb8f..fb684fa 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -2070,6 +2070,7 @@ export interface Translations { moderation: string; notifications: string; integrations: string; + finance: string; reports: string; partnersGroup: string; sellerManagement: string;