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 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-17 23:56:59 +04:00
parent e3a70f5e65
commit a4f44dbb58
14 changed files with 208 additions and 0 deletions

View File

@@ -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<ReconciliationRecord[]>([]);
readonly settlements = signal<Settlement[]>([]);
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());
}
}

View File

@@ -0,0 +1,29 @@
<div class="finance-page">
<header>
<h1>Payments &amp; Finance</h1>
<p>Reconciliation queue and settlements. Mock-gateway backed until docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md ships.</p>
</header>
<section>
<h2>Reconciliation queue</h2>
@if (facade.reconciliation().length === 0 && !facade.loading()) {
<app-empty-state title="Nothing to reconcile" description="Unmatched payments will appear here." />
} @else {
<ul>
@for (r of facade.reconciliation(); track r.id) {
<li>
{{ r.orderId }} - {{ r.result }}
<app-button variant="secondary" (click)="facade.resolve(r.id, 'resolved from backoffice')">Resolve</app-button>
</li>
}
</ul>
}
</section>
<section>
<h2>Settlements</h2>
@if (facade.settlements().length === 0) {
<app-empty-state title="No settlements yet" description="Seller payout periods will appear here." />
}
</section>
</div>

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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'] },