fix(subscription): plan comparison + checkout read the live plan catalog
CI/CD / CI · API (dotnet build + test) (push) Successful in 42s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m10s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 47s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 3m27s

The merchant plan page hard-coded 4 tiers, prices and a feature matrix
that drifted from the admin-editable platform catalog (Starter tier
missing, stale prices/features). PlanComparison and CheckoutScreen now
consume /platform/plans + new /platform/features-catalog:

- columns = active plans by SortOrder (incl. Starter), names from
  DisplayNameFa/En, prices from MonthlyPriceToman
- limit rows from PlanLimitsData (int.MaxValue → "نامحدود")
- feature rows from the feature catalog, ticked via FeatureKeys
- checkout validates the ?plan= param against isBillableOnline and
  prices from the catalog — no more client-side price constants

fa/en/ar limit-row labels added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-12 08:16:29 +03:30
parent 74f46a4781
commit 615d5348de
7 changed files with 328 additions and 255 deletions
@@ -0,0 +1,75 @@
import { useQuery } from "@tanstack/react-query";
import { apiGet } from "@/lib/api/client";
/** Backend sends int.MaxValue for "no limit". */
export const UNLIMITED = 2147483647;
export interface PlanLimits {
maxOrdersPerDay: number;
maxTables: number;
maxTerminals: number;
maxCustomers: number;
maxSmsPerMonth: number;
maxBranches: number;
maxReportHistoryDays: number;
maxMenuCategories: number;
maxMenuItems: number;
maxMenuAi3dPerMonth: number;
}
export interface PlatformPlan {
tier: string;
displayNameFa: string;
displayNameEn?: string | null;
monthlyPriceToman: number;
isBillableOnline: boolean;
isActive: boolean;
sortOrder: number;
limits: PlanLimits;
featureKeys: string[];
}
export interface PlatformFeature {
id: string;
key: string;
displayNameFa: string;
displayNameEn?: string | null;
moduleGroup: string;
isEnabledGlobally: boolean;
}
/** Live, admin-editable plan matrix — the single source of truth for plan
* names, prices, limits, and included features. */
export function usePlatformPlans(cafeId?: string | null) {
return useQuery({
queryKey: ["platform-plans", cafeId],
queryFn: async () => {
const plans = await apiGet<PlatformPlan[]>(`/api/cafes/${cafeId}/platform/plans`);
return plans
.filter((p) => p.isActive)
.sort((a, b) => a.sortOrder - b.sortOrder);
},
enabled: !!cafeId,
staleTime: 5 * 60_000,
});
}
export function usePlatformFeaturesCatalog(cafeId?: string | null) {
return useQuery({
queryKey: ["platform-features-catalog", cafeId],
queryFn: () =>
apiGet<PlatformFeature[]>(`/api/cafes/${cafeId}/platform/features-catalog`),
enabled: !!cafeId,
staleTime: 5 * 60_000,
});
}
export function planDisplayName(plan: PlatformPlan, locale: string): string {
if (locale === "en" && plan.displayNameEn) return plan.displayNameEn;
return plan.displayNameFa;
}
export function featureDisplayName(feature: PlatformFeature, locale: string): string {
if (locale === "en" && feature.displayNameEn) return feature.displayNameEn;
return feature.displayNameFa;
}