# Edge Cases & Unresolved Behaviors

> **Design notes for future phases.** These edge cases were identified during the design review process. Some are resolved, others are deferred.
>
> **TODO:** Review each edge case when implementing the relevant phase and mark as resolved or create tickets.

## Purpose & Audience

This document captures behaviors that must be explicitly decided or verified during implementation. Each edge case was surfaced during the design spec review. It is intended for engineers implementing the platform service to ensure nothing is overlooked. For the full architecture, see [platform architecture](../../architecture/platform.md). For the migration plan, see [migration cutover](../../operations/platform-migration-cutover.md).

---

## Tenant Lifecycle

### Tenant Hard Deletion

**Status:** Deferred to Phase 2.

**Problem:** `RequestDataDeletion` creates a lifecycle event but the spec does not describe cascade behavior for all tables. Audit logs have crypto-shredding for users but no equivalent for tenant-level deletion.

**Decision:**

- **Phase 1:** Tenants are only suspended/deactivated, never hard-deleted.
- **Phase 2:** Add `DeleteTenant` RPC that cascades all tenant-scoped tables, crypto-shreds tenant-level audit logs, and purges S3 archives. Require super admin + ExternalReference (legal hold check) before proceeding.

### Last Admin Deactivated

**Status:** Resolved -- enforce at handler level.

**Problem:** If the only user with `admin` role is deactivated, no one can manage the tenant.

**Decision:** `DeactivateUser` RPC must check: if user holds `admin` role and is the last active admin in the tenant, reject with `FAILED_PRECONDITION: "Cannot deactivate the last tenant admin."` Invariant: `inv-last-admin-guard`.

**Note:** The guard should check for "last user with `admin.users.manage` permission" (the ability to manage users), not the `admin` role slug. Custom roles with equivalent permissions would bypass a slug-based check (see `inv-last-admin-permission-check`).

### Pending Invitations on Tenant Suspension

**Status:** Resolved -- documented as intentional.

**Problem:** What happens to pending invitations when a tenant is suspended?

**Decision:** Invitations remain `pending` during suspension. On reactivation, unexpired invitations are still valid. Suspended tenants' invitation links return `403: "Tenant suspended"` at `AcceptInvitation`. No auto-revocation on suspension.

### Multi-Tenant User with One Suspended Tenant

**Status:** Resolved -- no cross-tenant impact.

**Problem:** User is in Tenant A (active) and Tenant B (suspended).

**Decision:** Each tenant is independent. Suspension of Tenant B does not affect Tenant A. `ResolveTenant` resolves to the specific tenant per request. User's Tenant A sessions and permissions are unaffected.

---

## Plan and Add-On Management

### Add-On Incompatible with Migration Target

**Status:** Must enforce at migration time.

**Problem:** A tenant has `opus-access` add-on on Professional v1. Plan migrates to Professional v2 which does not include `opus-access` in `compatiblePlans`.

**Decision:** `MigrateTenantPlan` must check all active add-ons against the target plan's compatibility. Incompatible add-ons are flagged -- admin must cancel them before migration, or migration applies `featureOverrides` to preserve access (grandfathering).

---

## SCIM Integration

### In-Flight SCIM Sync When Token Is Revoked

**Status:** Resolved -- follows SCIM 2.0 spec.

**Problem:** IdP is syncing 500 users. Token revoked mid-sync.

**Decision:** Subsequent SCIM requests return `401` per SCIM 2.0 format. Already-provisioned users remain active. The sync is partial -- IdP will retry on next cycle with a new token. No rollback of already-provisioned users.

---

## Resource Sharing

### Resource Share Cascade on Parent Delete

**Status:** Must enforce in application layer.

**Problem:** Deleting a dashboard/data source/chat should delete its `ResourceShare` rows. But `ResourceShare` uses polymorphic `resourceType + resourceId` with no FK.

**Decision:** Application layer must `DELETE FROM resource_shares WHERE resourceType = ? AND resourceId = ?` before or after deleting the parent resource. Invariant: `inv-share-cascade-on-resource-delete`.

---

## Webhooks

### Webhook Retry/Backoff Strategy

**Status:** Resolved -- exponential backoff.

**Problem:** `WebhookDelivery` has `attempts` and `nextRetryAt` but retry semantics were undefined.

**Decision:**

- Exponential backoff: 30s, 2min, 10min, 1hr, 6hr, 24hr (6 retries over ~31 hours)
- Failure = non-2xx HTTP, timeout (10s), TLS error
- After 6 consecutive failures, transition `Webhook.status` to `failed` and notify tenant admin
- Tenant admin can manually retry or fix the endpoint, then reactivate
- Maximum payload size: 64KB

---

## Compliance

### Business-Day SLA Calendars

**Status:** Deferred -- requires holiday calendar integration.

**Problem:** LFPDPPP uses "20 business days" (excluding Mexican holidays), PIPL uses "15 business days." SLA computation needs jurisdiction-specific calendars.

**Decision:** `privacy-sla-tracker.ts` must import a holiday calendar per jurisdiction. Recommended: use an open-source holiday library (e.g., `date-holidays` npm package). Store the computed deadline as an absolute `DateTime` in `PrivacyRequest.slaDeadline` at creation time -- do not recompute. Timezone for "day 1": use the tenant's jurisdiction's primary timezone (e.g., `America/Mexico_City` for MX, `Asia/Tokyo` for JP).

---

## Authorization

### Impersonation Token Scope

**Status:** Resolved -- specified in ImpersonateResponse proto.

**Problem:** What does an impersonation token grant? How does it interact with the pipeline?

**Decision:** Token runs the pipeline as the target user (not as the super admin). Session-tracked. Tenant-bound. Max 1 hour. Step-up still applies. `product`-level super admins can only impersonate in tenants where `is_test: true`.

### Groups Quota Enforcement

**Status:** Resolved -- add to enforcement table.

**Problem:** `groups` quota (3/10/50/unlimited) is listed but was missing from the quota enforcement table.

**Decision:** Add to enforcement: "Create group -> `groups` count (per-tenant) -> GroupService, before DB insert."

---

## Pricing and Credits

### Opus Margin Risk

**Status:** Open product decision.

**Problem:** Business plan Opus credit cost (20) yields negative margin on heavy Opus users.

**Options:** (a) increase to 25 credits, (b) add per-user Opus monthly cap (e.g., max 1,000 credits on Opus per user per month), (c) accept with monitoring. Decision required before pricing launch.

### Promotion Stacking

**Status:** Must enforce limits.

**Problem:** Multiple promotions could stack to produce compound discounts exceeding 50%.

**Decision:** Add `maxActivePromotions` per tenant (default 1 for `plan_discount` rewards, unlimited for `bonus_credits`). Or: add exclusion groups -- only one `plan_discount` reward active at a time.

### Chat Retention on Starter

**Status:** Open product decision.

**Problem:** 90-day chat retention on Starter is aggressive.

**Decision:** Archived chats must remain accessible via a read-only archive UI. Minimum recommended: 180 days for Starter, or expose archived chats as searchable history.

### Free Trial

**Status:** Open product decision.

**Problem:** No default free trial or free tier exists.

**Decision:** Recommended: 14-day free trial on Professional for self-serve signups. The promotion system can model this as a `trial` promotion auto-applied at `SignupTenant`.

### Billing Integration

**Status:** Separate spec required.

**Problem:** Credit metering, overage billing, seat changes, proration, and add-on purchasing all need a billing system (Stripe or Chargebee).

**Decision:** Define: who is system of record (Stripe or platform service), how overages are invoiced, how seat changes trigger proration. Add to backlog as separate design spec.

---

## Data Model Constraints

### User Relations

Models must include back-relations for all new associations: `groupMemberships UserGroupMember[]`, `sessions Session[]`. Without these, `prisma generate` will fail.

### Cascade Policies

Define `onDelete` for all relations:

- **Restrict** for append-only tables (AuditLog, CreditEntry, ConsentRecord, PermissionChangeLog)
- **Cascade** for child tables (PlanFeature, PlanCreditCost, RolePermission, RoleInheritance, ScimToken on tenant delete)
- **SetNull** for optional references (TenantUser.departmentId)

### Sentinel Nulls

Columns in unique constraints use `"*"` sentinel with `@default("*")` instead of nullable (MySQL InnoDB treats NULL != NULL). Applies to: `RateLimit.scopeId`/`tenantId`, `ResourceQuota.scopeId`/`tenantId`, `ResourceShare.targetId` for `shareTarget=tenant`. API validation must reject `"*"` as input from callers -- only the service layer may set sentinels internally.

### CreditEntry Growth

`CreditEntry` table grows unboundedly (~365M rows/year at scale). Add archival strategy: archive entries older than the plan's `audit.retention_days` to S3, then delete. Same pattern as audit log archival.

---

## API Constraints

### Pagination

All List RPCs must support cursor-based pagination via `page_size` + `page_token`. Default page size: 50. Max: 200. Defined in shared `ows/common/v1/pagination.proto`.

### Field Masks

All Update RPCs must support `google.protobuf.FieldMask` for partial updates.

### Missing RPCs

Add RPCs for: `ChatAttachmentService` (upload via presigned URL, list, delete), `WebhookDeliveryService` (list delivery history), `ServiceAccountService` (suspend, reactivate, revoke, delete, update).

### Missing Proto Enums

Add proto enum counterparts for: `ShareTarget`, `ShareAccessLevel`, `TenantPlanStatus`, `TenantAddOnStatus`, `AddOnStatus`, `CodeType`, `CodeStatus`, `RedemptionStatus`, `ServiceAccountStatus`, `StepUpMethod`, `StepUpStatus`, `WebhookStatus`, `AttachmentSource`, `ScanStatus`, `AccessRequestType`.
