20 KiB
Boilerplate-Mapping — M28 Subscriptions / Billing
Modul-ID: M28
Name: Subscriptions / Billing
Milestones: M153 (Stripe-Integration EU), M154 (Plan-/Seat-Modell), M155 (Trial + Read-Only), M156 (Metering / Pay-as-you-go)
Pflichtenheft-Block: Block 7, Modul 28 (_bmad-output/pflichtenheft/blocks/block7.md §28.1–28.Y)
Detail-Spec: _bmad-output/specs/M28-subscriptions-billing.md
Reuse-Level: HIGH (Daten-Layer, Stripe-Service-Layer, Pricing-Config-Admin, Pay-as-you-go-Engine fast vollständig vorhanden; Lifecycle-State-Machine, Seat-Modell und Read-Only-Middleware fehlen)
Verifiziert direkt gegen
/Users/raffaelsheikh/Documents/GitHub/boilerplate(nicht nur Inventur). Wichtig: Die BP-Modelle bilden den Stripe-Spiegel struktur-getreu ab, weichen aber in mehreren spec-kritischen Punkten ab (kein Status-Enum, kein Seat-Typ, andere Idempotenz-Strategie, sterilere Felder).
Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
prisma/schema/subscriptions.prisma |
Komplettes Subscription-Daten-Modell: SubscriptionProduct, SubscriptionPrice, SubscriptionUsageBasedPrice, SubscriptionUsageBasedTier, SubscriptionFeature, TenantSubscription, TenantSubscriptionProduct, TenantSubscriptionProductPrice, TenantSubscriptionUsageRecord, TenantProductConfiguration, TenantFeatureOverride, CheckoutSessionStatus, Credit |
anpassen — Modellnamen 1:1 deckungsgleich mit Spec; Felder müssen erweitert werden (siehe Lücken). Als Migrations-Basis übernehmen. |
apps/web/app/utils/stripe.server.ts (21 KB) |
Stripe-Client + ~40 Funktionen: createStripeCheckoutSession, createStripeSetupSession, createStripeSubscription, cancelStripeSubscription, reactivateStripeSubscription, createCustomerPortalSession, getStripeInvoices, getStripeUpcomingInvoice(s), getStripePaymentMethods, createStripeCustomer, Products/Prices CRUD, createStripeUsageBasedPrice, createMeterEvent, listMeterEventSummaries, getStripeCoupon/PromotionCode, Revenue-Aggregation |
1:1 — deckt fast alle Stripe-Operationen der Spec (Block C, F, H, I, K) ab. API-Version 2025-10-29.clover. EU-Entity/Tax-Felder konfigurativ. |
apps/web/app/utils/services/.server/subscriptionService.ts (23 KB, mit Tests) |
getPlanFeaturesUsage, getPlanFeatureUsage, getActiveTenantSubscriptions, reportUsage, persistCheckoutSessionStatus, cancelTenantSubscription, getMrr, convertToCurrency, clearTenantSubscriptionCache |
anpassen — Lifecycle-Helper als Basis; Status-Übergänge (Block B) fehlen und sind zu ergänzen. |
apps/web/app/utils/services/.server/subscriptionCache.server.ts |
Two-Tier-Cache (L1 in-process Map, L2 SharedStore-Pub/Sub) mit TTL-Default 5 min + Cross-Replica-Invalidation | anpassen — Spec verlangt 30 s TTL (Block G) statt 5 min; ansonsten 1:1 das geforderte Cache-Invalidate-Pattern. |
apps/web/app/utils/services/.server/pricingService.ts (23 KB) |
Pricing-Berechnung / Plan-Preis-Auflösung | anpassen — Basis für Setup-Fee-Differenz-Logik (Block K) und Proration. |
apps/web/app/utils/services/.server/tenantPlanLookup.server.ts (+Test) |
Plan-Auflösung pro Tenant | 1:1 / anpassen — direkt für Quota-/Feature-Lookup. |
apps/web/app/utils/db/subscriptions/tenantSubscriptionProducts.db.server.ts (+Test) |
DB-Layer Tenant-Subscription-Products | als-vorlage — DB-Zugriffsmuster; in HiveGrid-Layer-Trennung (View-Model) zu überführen. |
apps/web/app/utils/db/stripe/checkoutSessions.db.server.ts |
Checkout-Session-Persistenz | als-vorlage |
packages/entitlements/ (Paket @toolctrl/entitlements) |
Plan-/Feature-Gate-Engine: evaluateEntitlement, evaluateProductAccess, featureGateRegistry, featureKeys, getFeatureGate, standardEntitlementPlans, getProductBlueprint (blueprints.ts, aktuell crm-saas) |
anpassen — direkt nutzbar als Feature-Gating-Querschnitt (Spec SubscriptionFeature/SubscriptionProductFeature); HiveGrid-Plan-Codes/Blueprints einzufüllen. Plan-IDs sind aktuell free/pro/enterprise. |
packages/entitlements/src/ai-quotas.ts (+Test) |
resolveTenantAIQuota, getAIQuotaPlanDefaults, parseTenantAIQuotaOverride, aiQuotaCapabilityKeys |
1:1 / anpassen — deckt AI-Credits-Quota (Block J) konzeptionell ab. |
packages/entitlements/src/ai-overage.ts (+Test) |
resolveTenantAIOverage, computeMarkedUpCostCents, roundUpToBlock, standardAIOverageDefaults (Multiplier 1.5, Block-Cents) |
1:1 — Pay-as-you-go-Aufschlags-Rechnung (Block I/J + Pflichtenheft 28.8) bereits implementiert. |
apps/worker/src/jobs/ai-cost-aggregate*.ts, ai-cost-persist*.ts, ai-cost-stripe-report*.ts, ai-cost-stripe-submitter.ts, ai-cost-reconciliation*.ts, ai-cost-alert.ts |
Voller AI-Cost-Pipeline-Stack: Aggregation pro Period → Persist → Stripe-Usage-Reporting (createMeterEvent) → Reconciliation → Kosten-Alert (Notbremse-Kandidat 28.8.6) |
1:1 / anpassen — deckt Metering-Reporting an Stripe (Block I) + Notbremse/Upsell-Trigger weitgehend ab. |
apps/worker/src/jobs/trial-expiring-trigger.ts (+Test) |
Täglicher Cron: scannt Trials mit Ablauf in Horizon (default 3 Tage), emittiert tenant.trial_expiring-Outbox-Event; idempotenter dispatchKey tenant.trial_expiring.<tenantId>.<YYYY-MM-DD> |
anpassen — Trial-Ende-3-Tage-/1-Tag-Warnung (Block E / 28.3.5). 1-Tag-Schwelle ergänzen. |
apps/api/src/billing/stripe-webhook-route.ts (+ 24 KB Test) |
Stripe-Webhook-Endpoint (Hono): Signatur-Verify (constructStripeWebhookEvent), Metrics, transaktionaler Sub-Update + Outbox-Publish, Cache-Invalidation. Handhabt: customer.subscription.updated/deleted, subscription_schedule.canceled |
anpassen — solide Endpoint-/Signatur-/Transaktions-/Outbox-Vorlage. Event-Coverage und Idempotenz-Strategie müssen erweitert werden (siehe Lücken). |
apps/web/app/components/core/settings/subscription/* |
UI: ChangeSubscription, MyInvoices, MyPayments, MyPaymentMethods, MyUpcomingInvoice, MyBillingSession, MyPlanFeatureUsage, MySubscriptionFeatures, MyProducts, Plan(s), PlanChangePreviewModal, ToggleBillingPeriod, CheckPlanFeatureLimit (alle mit i18n-Tests) |
als-vorlage — Achtung LP-8/Apple-Compliance: Pricing-/Plan-Vergleich/Upgrade-UI darf NICHT in die App. Nur Read-Only-Teile (MyInvoices, MyPaymentMethods, Usage-Anzeigen, Billing-Session-Embed) sind in HiveGrid zulässig; Plan-Wechsel/Toggle-Period gehören in Anti-Liste. |
apps/web/app/routes/admin/settings/pricing*.tsx (pricing.tsx, pricing.new.tsx, pricing.edit.$id.tsx, pricing.features.tsx, pricing.ai-overage.tsx) |
Plattform-Admin-Pricing-Konfiguration (datengetrieben, kein Code-Release) | anpassen — erfüllt Spec-Grundprinzip Block A (konfig-getriebene Engine) + Pflichtenheft 28.1.6. Gehört nach admin.hivegrid.de (M37). |
apps/web/app/routes/admin/accounts/subscriptions.tsx, subscriptions-revenue.tsx |
Plattform-Admin: alle Tenant-Subscriptions + Revenue-Sicht | anpassen — Spec Block N „Plattform-Admin-Sicht" (M37). |
apps/web/app/routes/app.$tenant/settings/subscription.tsx (+Test) |
Tenant-seitige Subscription-Settings-Route | als-vorlage — Route-Pattern + Loader-Muster; HiveGrid-Routes sind /{tenant}/admin/subscription/... (Spec Block M). |
packages/shared-store/ (@toolctrl/shared-store) |
Redis-KV: createIdempotencyStore, createTwoTierCache, Rate-Limiter |
1:1 — Idempotenz (AV #8) + Status-Cache (Block G) + Cross-Replica-Invalidation. |
prisma/schema/outbox.prisma + @toolctrl/db/outbox (publishOutboxEvent) |
Transactional-Outbox für zuverlässige Side-Effects (Notifications, Lifecycle-Events) | 1:1 — Übergangs-Hooks (Block B) sauber als Outbox-Events. |
apps/worker/src/jobs/legacy-event-receiver.ts |
Outbox→BullMQ-Consumer für subscription.ended/cancelled Lifecycle-Events |
anpassen — Vorlage für Status-Übergangs-Side-Effects (Audit/Activity/Notification). |
Vorhandene Daten-Modelle (konkret, mit Pfad)
Alle in /Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/subscriptions.prisma:
| BP-Modell | Spec-Pendant | Deckung |
|---|---|---|
SubscriptionProduct |
SubscriptionProduct |
Teilweise — hat stripeId, order, title, active, model, public, badge, billingAddressCollection, hasQuantity, canBuyAgain, Relation assignsTenantTypes TenantType[]. Fehlt: code, tier_order, is_self_service_eligible, setup_fee_required, trial_default_days, requires_manual_activation, frozen_mode_price_id, locale-display_name, Markdown-description, created_at/updated_at. |
SubscriptionPrice |
SubscriptionPrice |
Teilweise — stripeId, type(Int), billingPeriod(Int), price(Decimal), currency, trialDays, active. Fehlt: interval-Enum (month/year/one_time), interval_count, unit_amount_cents, tax_behavior, valid_from/valid_until. |
SubscriptionFeature |
SubscriptionFeature + teilweise SubscriptionProductFeature |
Abweichend — BP-Feature ist direkt an SubscriptionProduct gehängt (name, type, value, accumulate), kein globaler Feature-Katalog + M:N-Join. Spec verlangt globalen SubscriptionFeature (mit category-Enum) + separate SubscriptionProductFeature (mit quota_value JSON). Umbau nötig. |
SubscriptionUsageBasedPrice |
SubscriptionUsageBasedPrice |
Gut — unit, usageType, aggregateUsage, tiersMode, billingScheme, Tiers. Fehlt: metric_code-Enum (automation_trigger/ai_token/sms_outbound), included_amount, overage_pricing_strategy. |
SubscriptionUsageBasedTier |
SubscriptionUsageBasedTier |
1:1 — from, to?, perUnitPrice, flatFeePrice. |
TenantSubscription |
TenantSubscription |
Stark abweichend — BP hat nur tenantId, stripeCustomerId, products[]. Fehlt alles Lifecycle-Relevante: status-Enum, subscription_product_id, stripe_subscription_id, Period-Felder, Trial-Felder, cancel_at_period_end, cancelled_at/reason, frozen_since, setup_fee_paid_at, Dunning-Felder, created_by_user_id, created_via. In BP liegen Period/Cancel-Felder auf TenantSubscriptionProduct, nicht auf TenantSubscription. |
TenantSubscriptionProduct |
TenantSubscriptionProduct (Snapshot) |
Teilweise — hat currentPeriodStart/End, cancelledAt, endsAt, stripeSubscriptionId, quantity, fromCheckoutSessionId, customEntitlementPlanId. Fehlt: snapshot_data (JSONB Preisstabilität), valid_from/valid_until. |
TenantSubscriptionProductPrice |
TenantSubscriptionProductPrice |
Teilweise — verknüpft Price + UsageBasedPrice + UsageRecords. Fehlt: quantity (liegt eine Ebene höher), is_metered-Flag, valid_from/valid_until. |
TenantSubscriptionUsageRecord |
TenantSubscriptionUsageRecord |
Abweichend — BP: timestamp(Int), quantity, stripeSubscriptionItemId. Spec: metric_code, period_start/end, usage_amount, reported_to_stripe_at, Composite-Index. Erweitern. |
CheckoutSessionStatus |
CheckoutSessionStatus |
Teilweise — BP: pending(Bool), email, fromUrl, from/createdUserId, from/createdTenantId. Fehlt: stripe_checkout_session_id, status-Enum, intent-Enum, completed_at/expires_at, error_message, redirect_url_*. |
Credit |
Credit |
Teilweise — BP: tenantId, userId?, amount(Int), type(String), objectId?. Fehlt: credit_type-Enum, source-Enum, valid_from/valid_until, balance_after, notes, created_by_user_id-Semantik. |
TenantProductConfiguration |
(kein direktes Spec-Pendant) | Bonus — blueprintId default crm-saas. Brauchbar als Branchen-/Blueprint-Anker (M29). |
TenantFeatureOverride |
(deckt quota_value-Override-Idee) |
Bonus — Per-Tenant Feature-Override (featureKey/override/reason). Nutzbar für manuelle Plattform-Admin-Grants. |
Verwandt, NICHT subscriptions.prisma:
prisma/schema/accounts.prisma→model Tenanthat nuractive: Boolean+deactivatedReason: String?— kein Status-Enum (trial/active/past_due/cancelled/frozen). Die in Spec Block B geforderteTenant.status↔TenantSubscription.status-Synchronisation existiert in der BP NICHT.model TenantType(accounts.prisma) — ist ein Tenant-Typ (vonSubscriptionProduct.assignsTenantTypeszugewiesen), nicht das Büro/Monteur-User-Seat-Konzept der Spec (Block D). Kein Seat-Typ pro User vorhanden.prisma/schema/portals.prisma→ eigenes gespiegeltesPortalSubscription*-Subsystem (Endkunden-Billing). Nicht für M28-Tenant-Billing, aber als zweites Muster vorhanden.
Lücken / komplett neu zu bauen
-
Subscription-Lifecycle-State-Machine (Block B) — fehlt komplett.
- Kein
status-Enum aufTenantSubscriptionund keinTenant.status-Enum. BP kennt nurTenant.active(Boolean). - Keine Übergangs-Tabelle/Guards (
trial→active,active→past_due,→frozen,→cancelled, Reaktivierung). BP-Webhook setzt nurcancelledAt/endsAt/Period. - Tenant-Status-Synchronisation (M27 A.5) neu zu bauen.
- Kein
-
Seat-Modell Büro/Monteur (Block D) — fehlt komplett.
- Kein per-User-Seat-Typ, keine Seat-Counter, keine Rolle↔Seat-Validierung, keine Quota-Enforcement (
max_buero_seats/max_monteur_seats), kein Hard/Soft-Limit.quantityexistiert nur als undifferenzierte Stripe-Item-Menge.
- Kein per-User-Seat-Typ, keine Seat-Counter, keine Rolle↔Seat-Validierung, keine Quota-Enforcement (
-
Read-Only-/Downgrade-Middleware (Block G, AV #11) — fehlt komplett.
- Grep nach
read.?only|past.?due|frozen|isWritable|402inapps/web+apps/apiergibt KEINEN Billing-Status-Gate. Keinis_writable_status()-Helper, kein 402-Response auf Write-Endpoints, keine Worker-Job-Blockade (Automation/Notification/Webhook-Outbound), keine UI-Disable-Logik. Komplett neu, gekoppelt an Lifecycle (#1).
- Grep nach
-
Webhook-Event-Coverage + Idempotenz-Tabelle (Block C) — teilweise.
- BP-Handler deckt nur
customer.subscription.updated/deleted+subscription_schedule.canceled. Fehlen:checkout.session.completed,invoice.created,invoice.payment_succeeded,invoice.payment_failed,customer.subscription.trial_will_end. - Spec verlangt
StripeWebhookLog-Tabelle mitstripe_event_id-Unique-Constraint (Schreib-Idempotenz). BP nutzt stattdessen Outbox-dispatchKey(stripe:<eventId>:...) — funktional ähnlich, aber NICHT die geforderte Tabelle. Neu anzulegen oder Spec-Abweichung freigeben.
- BP-Handler deckt nur
-
Dunning / Past-Due-Tracking (Block F) — fehlt.
- Keine
last_dunning_attempt_at/dunning_attempt_count-Felder, keine 14-Tage-3-Versuche-Logik, keine gestaffelte Notification-Strategie.invoice.payment_failed-Handling fehlt (siehe #4).
- Keine
-
Frozen-Modus / User-Pause (Block H) — fehlt.
- Kein
frozen_since, keinfrozen_mode_price_id, kein „PAUSIEREN"-Multi-Step-Flow, kein Stripe-Price-Wechsel auf reduzierte Gebühr, keinfrozen→past_due-Pfad.
- Kein
-
Trial-Aktivierungs-Flow mit Vertriebs-Freigabe (Block E) — teilweise.
trial-expiring-trigger.tsdeckt die Warnungen ab. Fehlt:requires_manual_activation-Flow (Vertrieb setzttrial_start/trial_end), „Trial-Anfragen"-Liste in M37, Trial-Ende→deletion_pending-Übergang, 1-Tag-Schwelle.
-
AI-Credits-Saldo + In-App-Sichten (Block J) — teilweise.
- Engine-Teile (
ai-quotas,ai-overage,Credit-Modell) da. Fehlt: Saldo-Berechnung ausCredit-Sum mit Carry-Over/valid_until-Enforcement,balance_after, Pre-Check-Gate beim LLM-Call mit Klartext-Fehler, In-App-Sichten/{tenant}/admin/usage/+/usage/ai-credits/(Listen, Drill-Down, Trend-Chart, CSV-Export, Hochrechnung).
- Engine-Teile (
-
Setup-Fee-Differenz-Logik (Block K) — fehlt.
setup_fee_required,setup_fee_paid_at,one_time-Interval-Handling, Differenz-Berechnung bei Up-Grade (effective_setup_fee = neu - bisherig, clamped ≥0).
-
Layer-Trennung (AGENTS.md DoD A) — kompletter Umbau.
- BP-Code mischt DB + UI in
apps/web/app/components/core/settings/subscription/*und nutzt.db.server.ts/.server.tsdirekt. HiveGrid verlangt: View-Models inpackages/view-models/, UI inpackages/ui-hivegrid/(kein Prisma-Import), Subframe via Adapter, Routes bauen VM. Alle BP-UI-Komponenten sind daher nur als-vorlage brauchbar, nicht 1:1.
- BP-Code mischt DB + UI in
-
Apple-Compliance-Filter (LP-8 / 28.X.10) — neu durchzusetzen.
- BP-UI enthält explizit Pricing/Plan-Vergleich/Upgrade/Toggle-Period (
Plans.tsx,Plan.tsx,ChangeSubscription.tsx,ToggleBillingPeriod.tsx,PlanChangePreviewModal.tsx). Diese gehören in HiveGrid auf die Anti-Liste; in der App nur Read-Only + externer „Plan-Wechsel über hivegrid.de"-Link (noopener noreferrer).
- BP-UI enthält explizit Pricing/Plan-Vergleich/Upgrade/Toggle-Period (
-
Querschnitt-Wiring (AGENTS.md DoD B/C) — anzudocken.
- Sidebar-Eintrag „Subscription / Abo" (Gruppe EINSTELLUNGEN, INTEGRATION-CONTRACTS §1) + Permission-Gate, Breadcrumbs, i18n-Keys,
logActivity,notify()statt direkter Mail,withTenantScope()/withPermission(), Permission-Keysbilling.read/tenant.admin(Block N) inpackages/entitlements/src/permissions.ts. In der BP nicht in dieser Form vorhanden.
- Sidebar-Eintrag „Subscription / Abo" (Gruppe EINSTELLUNGEN, INTEGRATION-CONTRACTS §1) + Permission-Gate, Breadcrumbs, i18n-Keys,
-
Demo-Buchung (Block L) — bewusst extern (Calendly), kein Code nötig; nur
created_via=platform_admin-Pfad.
Empfohlenes Vorgehen (Reihenfolge)
M153 — Stripe-Integration (zuerst, Fundament):
prisma/schema/subscriptions.prismaals Migrations-Basis übernehmen; spec-Felder ergänzen (insb.TenantSubscription.status-Enum, fehlende Product/Price/Credit/CheckoutSession-Felder) undTenant.status-Enum inaccounts.prismaeinführen.apps/web/app/utils/stripe.server.ts1:1 übernehmen (in HiveGrid-Domain-Layerapp/domain/billing/); EU-Entity/Tax konfigurieren.StripeWebhookLog-Tabelle neu + Webhook-Handler ausapps/api/src/billing/stripe-webhook-route.tsals Vorlage erweitern auf alle Pflicht-Events (Block C). Idempotenz überstripe_event_id-Unique. Outbox-/Transaktions-Muster der BP beibehalten.- Idempotenz/Cache aus
@toolctrl/shared-store+subscriptionCache.server.ts(TTL auf 30 s).
M154 — Plan-/Seat-Modell:
5. Pricing-Config-Admin (routes/admin/settings/pricing*.tsx) anpassen → admin.hivegrid.de (datengetriebene Engine, Block A).
6. packages/entitlements als Feature-Gating übernehmen; globalen SubscriptionFeature-Katalog + SubscriptionProductFeature-M:N umbauen; HiveGrid-Plan-Codes/Blueprints einfüllen.
7. Seat-Modell (Büro/Monteur) komplett neu — neue Felder/Counter, Rolle↔Seat-Validierung, Quota-Enforcement, Stripe-quantity-Sync. Usage-Sicht /{tenant}/admin/usage/.
M155 — Trial + Read-Only:
8. Lifecycle-State-Machine + Tenant.status-Sync (Block B) neu; Übergangs-Hooks via Outbox (Audit/Activity/notify()).
9. Read-Only-/Downgrade-Middleware (Block G) komplett neu — Route-Guard + Worker-Guard + 402-API + UI-Disable; an Lifecycle gekoppelt.
10. Trial-Aktivierungs-Flow + Dunning (Block E/F): trial-expiring-trigger.ts erweitern (1-Tag), requires_manual_activation-Flow + Dunning-Counter + invoice.payment_failed-Handling.
11. Frozen-Modus (Block H) + Setup-Fee (Block K).
M156 — Metering / Pay-as-you-go (vieles vorhanden):
12. packages/entitlements/ai-overage.ts + ai-quotas.ts 1:1 übernehmen.
13. AI-Cost-Worker-Pipeline (ai-cost-*) übernehmen/anpassen (Stripe-Usage via createMeterEvent, Reconciliation, Notbremse-Alert 28.8.6).
14. AI-Credits-Saldo-Logik (Credit-Sum, Carry-Over, Pre-Check-Gate) + In-App-Sichten (Usage/AI-Credits, CSV-Export) neu bauen.
Querschnitt (alle Milestones): Layer-Trennung (View-Models + ui-hivegrid + Adapter), Sidebar/Permissions/Breadcrumbs/i18n, Apple-Compliance-Filter (keine Pricing/Upgrade-UI in der App).
Notizen
- Reuse-Grad HIGH trägt v.a. von Daten-Modell, Stripe-Wrapper, Pricing-Admin und Pay-as-you-go-Engine. Die drei spec-kritischsten Bereiche — Lifecycle-State-Machine, Seat-Modell Büro/Monteur, Read-Only-Middleware — sind in der BP NICHT vorhanden und komplett neu (das senkt es vom theoretischen „high+" auf solides high).
- Idempotenz-Konflikt: Spec fordert
StripeWebhookLog-Tabelle; BP nutzt Outbox-dispatchKey. Vor Bau entscheiden (Spec-Abweichung freigeben oder Tabelle bauen). - Apple-Compliance ist das größte „Anti"-Risiko: die fertigen BP-UI-Komponenten (
Plans.tsx,ChangeSubscription.tsx,ToggleBillingPeriod.tsx) verleiten zur Übernahme verbotener In-App-Pricing-UI. Nur Read-Only-Sichten + externer Link erlaubt. - BP-API-Version
2025-10-29.clover(apps/web/app/utils/stripe.server.ts:35ff.) — beim Übernehmen aktuell halten.