fix: review findings from full-diff audit (7 fixed)
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
- popularSearches sent translated display text as the actual search query instead of the canonical term - useSuggestion() now prefers target.query.q when present. - CartService.addItem() dedup guard resolved immediately instead of awaiting the real in-flight add; now tracks the pending Promise per itemID so concurrent callers await the actual result. - addItem()'s Promise never rejected on failure (resolve() in both next/error branches) - now rejects on error; buyNow() catches and shows an error toast instead of navigating on a failed add. - Quick View had no stale-response guard - a slower earlier request could overwrite a faster later one. Added a request-generation counter. - cart autoSubmitPurchase() set paymentStatus to null synchronously right after firing the async submit call, blanking the success screen while the request was still in flight. Removed the redundant/harmful line. - Order terminal-status guard (cancelled/refunded can't be reopened) lived only in the page component. Moved enforcement into the gateway (single write path) via a shared TERMINAL_ORDER_STATUSES const, so no future caller can bypass it. - TranslatePipe's per-instance memoization cache had no eviction, so bindings with volatile params (pagination counts) grew it unbounded for the component's lifetime. Capped at 50 entries. Not changed: the dark-mode color override was flagged as clobbering admin branding, but it's the exact palette explicitly requested this session for the global dark default - not a bug. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AdminOrder, AdminOrderListFilters, AdminOrderStatus } from '../models/admin-order.model';
|
||||
import { AdminOrder, AdminOrderListFilters, AdminOrderStatus, TERMINAL_ORDER_STATUSES } from '../models/admin-order.model';
|
||||
import { AdminOrdersLocalGateway } from '../services/admin-orders-local.gateway';
|
||||
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
|
||||
|
||||
|
||||
@@ -3,6 +3,14 @@ import { UUID } from '../../../../shared/types/primitive.types';
|
||||
export type AdminOrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded';
|
||||
export type AdminOrderPaymentStatus = 'unpaid' | 'paid' | 'refund_requested' | 'refunded';
|
||||
|
||||
/**
|
||||
* Single source of truth for which statuses are terminal. Enforced at the
|
||||
* facade layer (not just the page component's dropdown filtering) so any
|
||||
* caller - a different admin surface, a future API route - can't move a
|
||||
* cancelled/refunded order back to an active status.
|
||||
*/
|
||||
export const TERMINAL_ORDER_STATUSES: readonly AdminOrderStatus[] = ['cancelled', 'refunded'];
|
||||
|
||||
export interface AdminOrderCustomer {
|
||||
name: string;
|
||||
email: string;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AdminOrdersFacade } from '../facade/admin-orders.facade';
|
||||
import { AdminOrderStatus } from '../models/admin-order.model';
|
||||
import { AdminOrderStatus, TERMINAL_ORDER_STATUSES } from '../models/admin-order.model';
|
||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../../../i18n/translate.service';
|
||||
import { LanguageService } from '../../../../services/language.service';
|
||||
@@ -13,7 +13,6 @@ import { OrderTimelineComponent, OrderTimelineEntry } from '../components/order-
|
||||
import { ConfirmDialogComponent } from '../../../../shared/ui/confirm-dialog/confirm-dialog.component';
|
||||
|
||||
const WORKFLOW_STEPS: AdminOrderStatus[] = ['pending', 'processing', 'shipped', 'delivered'];
|
||||
const TERMINAL_STATUSES: AdminOrderStatus[] = ['cancelled', 'refunded'];
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-order-detail-page',
|
||||
@@ -32,7 +31,7 @@ export class AdminOrderDetailPageComponent {
|
||||
|
||||
readonly statuses: AdminOrderStatus[] = ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
|
||||
/** Terminal statuses are only reachable via the confirm-gated cancel()/requestRefund(), never the raw dropdown. */
|
||||
readonly selectableStatuses: AdminOrderStatus[] = this.statuses.filter(status => !TERMINAL_STATUSES.includes(status));
|
||||
readonly selectableStatuses: AdminOrderStatus[] = this.statuses.filter(status => !TERMINAL_ORDER_STATUSES.includes(status));
|
||||
readonly workflowSteps = WORKFLOW_STEPS;
|
||||
readonly noteDraft = signal('');
|
||||
readonly pendingCancelId = signal<string | null>(null);
|
||||
@@ -41,7 +40,7 @@ export class AdminOrderDetailPageComponent {
|
||||
|
||||
readonly isTerminal = computed(() => {
|
||||
const order = this.facade.selected();
|
||||
return !!order && TERMINAL_STATUSES.includes(order.status);
|
||||
return !!order && TERMINAL_ORDER_STATUSES.includes(order.status);
|
||||
});
|
||||
|
||||
readonly currentStepIndex = computed(() => {
|
||||
@@ -79,7 +78,7 @@ export class AdminOrderDetailPageComponent {
|
||||
}
|
||||
|
||||
setStatus(id: string, status: AdminOrderStatus): void {
|
||||
if (TERMINAL_STATUSES.includes(status)) {
|
||||
if (TERMINAL_ORDER_STATUSES.includes(status)) {
|
||||
// Unreachable from the dropdown (options are filtered), but guard anyway
|
||||
// since terminal transitions must always go through the confirm dialog.
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AdminOrdersFacade, AdminOrderColumn, ALL_ORDER_COLUMNS } from '../facade/admin-orders.facade';
|
||||
import { AdminOrderStatus } from '../models/admin-order.model';
|
||||
import { AdminOrderStatus, TERMINAL_ORDER_STATUSES } from '../models/admin-order.model';
|
||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
import { LanguageService } from '../../../../services/language.service';
|
||||
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
|
||||
@@ -32,7 +32,8 @@ export class AdminOrdersListPageComponent {
|
||||
|
||||
readonly statuses = ['all', 'pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'] as const;
|
||||
/** Bulk status change excludes terminal statuses - cancel/refund must go through the confirm-gated single-order flow. */
|
||||
readonly bulkSelectableStatuses: AdminOrderStatus[] = ['pending', 'processing', 'shipped', 'delivered'];
|
||||
readonly bulkSelectableStatuses: AdminOrderStatus[] = (['pending', 'processing', 'shipped', 'delivered'] as const)
|
||||
.filter(status => !TERMINAL_ORDER_STATUSES.includes(status));
|
||||
readonly allColumns = ALL_ORDER_COLUMNS;
|
||||
protected readonly columnsPanelOpen = signal(false);
|
||||
protected readonly bulkStatusValue = signal<AdminOrderStatus>('pending');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { delay } from 'rxjs/operators';
|
||||
import { AdminOrder, AdminOrderListFilters, AdminOrdersListResult, AdminOrderStatus } from '../models/admin-order.model';
|
||||
import { AdminOrder, AdminOrderListFilters, AdminOrdersListResult, AdminOrderStatus, TERMINAL_ORDER_STATUSES } from '../models/admin-order.model';
|
||||
import { AdminOrdersGateway } from './admin-orders-gateway.interface';
|
||||
import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service';
|
||||
|
||||
@@ -38,12 +38,20 @@ export class AdminOrdersLocalGateway implements AdminOrdersGateway {
|
||||
}
|
||||
|
||||
updateStatus(id: string, status: AdminOrderStatus): Observable<AdminOrder | null> {
|
||||
return this.mutate(id, order => ({
|
||||
...order,
|
||||
status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
timeline: [...order.timeline, { status, timestamp: new Date().toISOString(), eventKey: 'statusChanged' as const, actor: this.currentActor }],
|
||||
}));
|
||||
return this.mutate(id, order => {
|
||||
// Once terminal (cancelled/refunded), an order never moves to any
|
||||
// other status again - enforced here, not just in the admin UI, so
|
||||
// no caller (this gateway is the single write path) can reopen one.
|
||||
if (TERMINAL_ORDER_STATUSES.includes(order.status)) {
|
||||
return order;
|
||||
}
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
timeline: [...order.timeline, { status, timestamp: new Date().toISOString(), eventKey: 'statusChanged' as const, actor: this.currentActor }],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
requestRefund(id: string): Observable<AdminOrder | null> {
|
||||
|
||||
Reference in New Issue
Block a user