feat(landing,billing): public pricing, global discount, services bar & estimator

Backend
- Add platform-wide global discount (platform_settings: global_discount_percent),
  applied centrally in PricingCatalogService.computeTotalsFromDb so it reaches
  every real charge (previews, deploys, renewals, upgrades, invoices). Admin
  GET/PATCH /billing/settings/global-discount.
- Add unauthenticated PublicPricingController (catalog + calculate) for the
  public landing page, returning gross/net and the discount percentage.
- Bill application replicas by the user-selected footprint: app CPU/RAM/storage
  now all scale by replica count; the single-replica database stays unscaled.

Frontend
- Landing: Services bar (PaaS active, DBaaS, KaaS/LaaS "coming soon" with
  expandable runtime/database menus), transparent Pricing section (per-resource
  rate cards with cycle toggle + discount strikethrough), and a cost Estimator
  ("estimate your package").
- Optional services and the database are priced like runtimes: the estimator
  lets users pick their CPU/RAM/storage (and DB type) so the cost scales by need.
- Admin billing: global-discount editor.
- i18n: fa/en strings for services, pricing, estimator and global discount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-22 23:44:30 +03:30
parent a58142cc4a
commit f7974dd382
13 changed files with 1226 additions and 10 deletions
+86
View File
@@ -66,6 +66,32 @@ const en: Dictionary = {
ctaSecondary: 'Sign in',
scrollHint: 'Dive into the clouds',
},
services: {
title: 'Platform services',
subtitle: 'Everything you need to build and run your product, in one place.',
comingSoon: 'Coming soon',
explore: 'Click to explore',
paas: {
label: 'PaaS',
name: 'Platform as a Service',
desc: 'Ship your app straight from code to Kubernetes — automatic build and release.',
},
dbaas: {
label: 'DBaaS',
name: 'Database as a Service',
desc: 'Managed, reliable databases ready in seconds.',
},
kaas: {
label: 'KaaS',
name: 'Kubernetes as a Service',
desc: 'A dedicated, managed Kubernetes cluster for your team.',
},
laas: {
label: 'LaaS',
name: 'Logging as a Service',
desc: 'Centralized log collection, retention and analysis.',
},
},
value: {
lead: 'A ',
highlight: 'self-service PaaS',
@@ -103,6 +129,53 @@ const en: Dictionary = {
{ kpi: 'Iran-native', label: 'Optimized for Iranian users' },
],
},
pricing: {
title: 'Transparent pricing',
subtitle: 'Pay only for the resources you use. These rates are read straight from the platforms active plans.',
runtimeLabel: 'Application type',
cycle: { hourly: 'Hourly', monthly: 'Monthly', yearly: 'Yearly' },
perCycle: { hourly: 'per hour', monthly: 'per month', yearly: 'per year' },
discountBadge: '{p}% off',
free: 'Free',
resources: {
base_fee: 'Base fee',
cpu_per_core: 'CPU (per core)',
memory_per_gb: 'Memory (per GB)',
storage_per_gb: 'Storage (per GB)',
database_addon: 'Managed database',
custom_domain_addon: 'Custom domain + SSL',
},
currency: 'Toman',
note: 'Your final bill is based on actual usage; these are the per-unit rates for each resource.',
loadError: 'Could not load pricing.',
},
estimator: {
title: 'Estimate your package',
subtitle: 'Pick the resources you need and see an instant cost estimate.',
runtime: 'Application type',
cpu: 'CPU',
memory: 'Memory (RAM)',
storage: 'Storage',
replicas: 'Replicas',
database: 'Database',
databaseType: 'Database type',
options: 'Add-on services',
redis: 'Redis',
rabbitmq: 'RabbitMQ',
elasticsearch: 'Elasticsearch (logging)',
customDomain: 'Custom domain + SSL',
cores: '{n} cores',
gb: '{n} GB',
none: 'None',
estimatedTitle: 'Estimated cost',
perCycle: { hourly: 'per hour', monthly: 'per month', yearly: 'per year' },
cycle: { hourly: 'Hourly', monthly: 'Monthly', yearly: 'Yearly' },
youSave: '{p}% discount applied',
calculating: 'Calculating…',
cta: 'Build this package',
currency: 'Toman',
error: 'Could not calculate cost.',
},
finalCta: {
titleLead: 'The sky is clear; its time to ',
titleHighlight: 'go live',
@@ -1103,6 +1176,19 @@ const en: Dictionary = {
saveFailedShort: 'Failed to save',
hours: 'hours',
days: 'days',
globalDiscount: {
title: 'Platform-wide discount',
subtitle: 'A single percentage applied to every price and real invoice. 0 means no discount.',
label: 'Global discount %',
hint: 'A number between 0 and 100',
save: 'Save discount',
saving: 'Saving…',
saved: 'Global discount saved',
edit: 'Edit',
cancel: 'Cancel',
none: 'No platform-wide discount is currently active.',
active: 'A {p}% discount is active on all services.',
},
discounts: {
title: 'Discount codes',
subtitle: 'Percentage discounts on different services — public or for specific users.',
+86
View File
@@ -65,6 +65,32 @@ const fa = {
ctaSecondary: 'ورود',
scrollHint: 'به دلِ ابرها بزن',
},
services: {
title: 'سرویس‌های پلتفرم',
subtitle: 'هرچه برای ساخت و اجرای محصولت لازم داری، یک‌جا.',
comingSoon: 'به‌زودی',
explore: 'برای مشاهده کلیک کن',
paas: {
label: 'PaaS',
name: 'پلتفرم به‌عنوان سرویس',
desc: 'اپلیکیشنت را مستقیم از کد روی کوبرنتیز منتشر کن — بیلد و انتشار خودکار.',
},
dbaas: {
label: 'DBaaS',
name: 'دیتابیس به‌عنوان سرویس',
desc: 'دیتابیس‌های مدیریت‌شده و پایدار، آماده در چند ثانیه.',
},
kaas: {
label: 'KaaS',
name: 'کوبرنتیز به‌عنوان سرویس',
desc: 'کلاستر کوبرنتیز اختصاصی و مدیریت‌شده برای تیم تو.',
},
laas: {
label: 'LaaS',
name: 'لاگ به‌عنوان سرویس',
desc: 'جمع‌آوری، نگه‌داری و تحلیل متمرکز لاگ‌ها.',
},
},
value: {
lead: 'یک پلتفرمِ ',
highlight: 'PaaS خودسرویس',
@@ -102,6 +128,53 @@ const fa = {
{ kpi: 'بومیِ ایران', label: 'بهینه برای کاربر ایرانی' },
],
},
pricing: {
title: 'قیمت‌گذاری شفاف',
subtitle: 'فقط بابت منابعی که مصرف می‌کنی پرداخت کن. این نرخ‌ها مستقیماً از پلن‌های فعال پلتفرم خوانده می‌شوند.',
runtimeLabel: 'نوع اپلیکیشن',
cycle: { hourly: 'ساعتی', monthly: 'ماهانه', yearly: 'سالانه' },
perCycle: { hourly: 'هر ساعت', monthly: 'هر ماه', yearly: 'هر سال' },
discountBadge: '{p}٪ تخفیف',
free: 'رایگان',
resources: {
base_fee: 'هزینهٔ پایه',
cpu_per_core: 'پردازنده (هر هسته)',
memory_per_gb: 'حافظه (هر گیگابایت)',
storage_per_gb: 'فضای ذخیره (هر گیگابایت)',
database_addon: 'دیتابیس مدیریت‌شده',
custom_domain_addon: 'دامنهٔ اختصاصی + SSL',
},
currency: 'تومان',
note: 'هزینهٔ نهایی بر اساس مصرف واقعی شما محاسبه می‌شود؛ این اعداد نرخ واحد هر منبع هستند.',
loadError: 'دریافت قیمت‌ها ممکن نشد.',
},
estimator: {
title: 'پکیج خودت را تخمین بزن',
subtitle: 'منابع موردنیازت را انتخاب کن تا همین حالا برآورد هزینه را ببینی.',
runtime: 'نوع اپلیکیشن',
cpu: 'پردازنده (CPU)',
memory: 'حافظه (RAM)',
storage: 'فضای ذخیره‌سازی',
replicas: 'تعداد نمونه',
database: 'دیتابیس',
databaseType: 'نوع دیتابیس',
options: 'سرویس‌های جانبی',
redis: 'Redis',
rabbitmq: 'RabbitMQ',
elasticsearch: 'Elasticsearch (لاگ)',
customDomain: 'دامنهٔ اختصاصی + SSL',
cores: '{n} هسته',
gb: '{n} گیگابایت',
none: 'بدون',
estimatedTitle: 'برآورد هزینه',
perCycle: { hourly: 'در ساعت', monthly: 'در ماه', yearly: 'در سال' },
cycle: { hourly: 'ساعتی', monthly: 'ماهانه', yearly: 'سالانه' },
youSave: '{p}٪ تخفیف اعمال شد',
calculating: 'در حال محاسبه…',
cta: 'ساخت این پکیج',
currency: 'تومان',
error: 'محاسبهٔ هزینه ممکن نشد.',
},
finalCta: {
titleLead: 'آسمان صاف است؛ وقتِ ',
titleHighlight: 'زنده',
@@ -1102,6 +1175,19 @@ const fa = {
saveFailedShort: 'ذخیره ناموفق بود',
hours: 'ساعت',
days: 'روز',
globalDiscount: {
title: 'تخفیف کلی پلتفرم',
subtitle: 'یک درصد تخفیف که روی همهٔ قیمت‌ها و صورت‌حساب‌های واقعی اعمال می‌شود. ۰ یعنی بدون تخفیف.',
label: 'درصد تخفیف کلی',
hint: 'عددی بین ۰ تا ۱۰۰',
save: 'ذخیرهٔ تخفیف',
saving: 'در حال ذخیره…',
saved: 'تخفیف کلی ذخیره شد',
edit: 'ویرایش',
cancel: 'انصراف',
none: 'در حال حاضر تخفیف کلی فعال نیست.',
active: 'تخفیف {p}٪ روی همهٔ سرویس‌ها فعال است.',
},
discounts: {
title: 'کدهای تخفیف',
subtitle: 'تخفیف درصدی روی سرویس‌های مختلف؛ عمومی یا مخصوص کاربران خاص.',