Implementation notes, patterns, and development guidelines
Phase 1 Complete: The server-side API and backend implementation is complete. Client-side React components are planned for future implementation.
Location: apps/zooly-app/app/api/terms-setup/
apps/zooly-app/app/api/terms-setup/
├── approveIpTerms/
│ └── route.ts # POST endpoint handler
├── deleteIpTerms/
│ └── route.ts # DELETE endpoint handler
├── getOrCreateIpTerms/
│ └── route.ts # POST endpoint handler
└── listIpTerms/
└── route.ts # GET endpoint handler
Location: packages/app/srv/src/terms-setup/
packages/app/srv/src/terms-setup/
├── approveIpTerms/
│ └── approveIpTerms.ts # Core approval logic
├── deleteIpTerms/
│ └── deleteIpTerms.ts # Core deletion logic
├── getOrCreateIpTerms/
│ ├── getOrCreateIpTerms.ts # Core retrieval/creation logic
│ └── generateIpTerms.ts # Default terms generation
├── listIpTerms/
│ └── listIpTerms.ts # List all terms for account
└── functions/
├── formatIpTerms.ts # Shared formatting utility
├── auth.ts # Authentication/authorization helpers
└── responses.ts # Response helpers
Location: packages/db/src/
packages/db/src/
├── schema/
│ └── ipTerms.ts # Database schema definition
└── access/
└── ipTerms.ts # Database access functions
Location: packages/terms/
packages/terms/
├── src/
│ ├── index.ts # Main exports
│ └── enums/
│ └── ip-terms/
│ ├── voice-over/
│ │ ├── voice-over-terms.ts # Blueprint with defaults
│ │ └── formatIpTermsVoiceOver.ts # Formatter function
│ ├── image/
│ │ ├── image-terms.ts
│ │ └── formatIpTermsImage.ts
│ └── likeness/
│ ├── likeness-terms.ts
│ └── formatIpTermsLikeness.ts
All route handlers follow this authentication pattern:
resolveAccountId(cookieHeader, targetAccountId?) which:
getVerifiedUserInfo()getAccountByUserId(user.id)targetAccountId provided and differs from user's account:
user.roles?.includes("admin")accountId and userIdAuthorization checks happen at two levels:
resolveAccountId() checks admin role when accountId parameter is providedterm.accountId === accountId or user has admin roleAll layers follow consistent error handling:
try {
// Operation logic
return successResponse(data);
} catch (error) {
console.error("Operation failed:", error);
return errorResponse(error.message, statusCode);
}
Error Types:
All endpoints return standardized responses:
Success:
{
status: "success",
data: { /* endpoint-specific data */ }
}
Error:
{
status: "error",
error: "Error message"
}
Terms are formatted using a centralized function:
formatIpTerms(ipTerms, accountData)ipTermType@zooly/terms) generates:
shortTerms - Brief description with seller namebullets - Array of formatted bullet pointslongTerms - Full legal textpriceUSDollar by parsing compensationValue stringFormattedIpTermsResultWhen terms don't exist, default terms are generated:
generateIpTerms(ipTermType, accountData, createdBy)ipTermType:
VoiceOver → getDefaultVoiceOverTerms()Image → getDefaultImageTerms()Likeness → getDefaultLikenessTerms()@zooly/terms) returns Partial<NewIpTerms> with defaultscreateIpTerms()formatIpTerms()Location: packages/app/srv/src/terms-setup/functions/formatIpTerms.ts
Purpose: Formats raw database records into structured, human-readable format with legal text.
Signature:
function formatIpTerms(
ipTerms: IpTerms,
accountData: Account
): FormattedIpTermsResult
Behavior:
sellerName from accountData.displayName (defaults to empty string)ipTermTypeshortTerms, bullets, and longTermspriceUSDollar by parsing compensationValue decimal string to numberFormattedIpTermsResult with seller data and formatted termsLocation: packages/app/srv/src/terms-setup/functions/auth.ts
Purpose: Handles authentication and authorization, resolving target account ID.
Signature:
async function resolveAccountId(
cookieHeader: string | null,
targetAccountId?: string
): Promise<ResolvedAccount>
Behavior:
getVerifiedUserInfo(cookieHeader)getAccountByUserId(user.id)targetAccountId provided and differs from user's account:
user.roles?.includes("admin"){ accountId, userId }Location: packages/app/srv/src/terms-setup/getOrCreateIpTerms/getOrCreateIpTerms.ts
Purpose: Retrieves existing IP terms or generates new ones if they don't exist.
Signature:
async function getOrCreateIpTerms(
ipTermType: IpTermType,
accountData: Account,
createdBy: string
): Promise<FormattedIpTermsResult>
Behavior:
accountId and ipTermType (excluding soft-deleted)formatIpTerms()generateIpTerms() to create defaultsLocation: packages/app/srv/src/terms-setup/getOrCreateIpTerms/generateIpTerms.ts
Purpose: Creates a new IpTerms record with default values based on term type.
Signature:
async function generateIpTerms(
ipTermType: IpTermType,
accountData: Account,
createdBy: string
): Promise<FormattedIpTermsResult>
Behavior:
ipTermType@zooly/terms packagecreateIpTerms()formatIpTerms()Location: packages/app/srv/src/terms-setup/approveIpTerms/approveIpTerms.ts
Purpose: Updates IP terms with new data and marks them as approved.
Signature:
async function approveIpTerms(params: {
ipTermsId: string;
ipTerms: Partial<UpdateIpTerms>;
accountId: string;
userId: string;
}): Promise<FormattedIpTermsResult>
Behavior:
getIpTermsById(ipTermsId) (throws 404 if not found)term.accountId === accountId (admin check done at route level)ipTerms, sets ipApprove: true, updatedBy: userIdupdateIpTerms() (sets updatedAt explicitly)formatIpTerms()Location: packages/app/srv/src/terms-setup/deleteIpTerms/deleteIpTerms.ts
Purpose: Soft-deletes an IP terms record.
Signature:
async function deleteIpTerms(
ipTermsId: string,
accountId: string,
userId: string
): Promise<IpTerms>
Behavior:
getIpTermsById(ipTermsId) (throws 404 if not found)term.accountId === accountIdsoftDeleteIpTerms(ipTermsId, userId) which:
deletedAt to current timestampupdatedBy to userIdupdatedAt to current timestampLocation: packages/app/srv/src/terms-setup/listIpTerms/listIpTerms.ts
Purpose: Retrieves all IP terms for an account.
Signature:
async function listIpTerms(
accountId: string
): Promise<FormattedIpTermsResult[]>
Behavior:
listIpTermsByAccountId(accountId) (excludes soft-deleted, ordered by ipTermType)getAccountById(accountId)formatIpTerms(term, accountData)All database operations go through access functions in packages/db/src/access/ipTerms.ts:
Queries by ID, excluding soft-deleted records. Returns null if not found or soft-deleted.
Queries by accountId and ipTermType, excluding soft-deleted records. Returns null if not found.
Queries all terms for accountId, excluding soft-deleted records. Ordered by ipTermType ascending.
Inserts new term with explicit defaults for ipApprove and termsVersion. Uses Drizzle .returning() to get created record.
Updates term by ID, excluding soft-deleted records. Always sets updatedAt: new Date() explicitly. Returns updated row or null.
Sets deletedAt, updatedBy, and updatedAt to current timestamp. Filters to deletedAt IS NULL to prevent double-deletion. Returns soft-deleted record or null.
The entire implementation uses strong TypeScript typing:
@zooly/types packageImportant: compensationValue is stored as decimal string in database. Always use priceUSDollar from formatted response for numeric operations:
// ❌ Don't use directly
const price = parseFloat(ipTerms.compensationValue); // May be null, string conversion
// ✅ Use formatted response
const price = formattedResult.priceUSDollar; // Already converted, null-safe
Test individual functions in isolation:
Test database operations with test database:
Test API endpoints with authenticated requests:
Don't: Query ipTermsTable directly
Do: Use access functions from packages/db/src/access/ipTerms.ts
Don't: Query without checking deletedAt IS NULL
Do: Use access functions (they handle this automatically)
Don't: Parse compensationValue string directly
Do: Use priceUSDollar from formatted response
Don't: Update without setting updatedBy
Do: Always provide updatedBy when updating
Don't: Physically delete records
Do: Use softDeleteIpTerms() to preserve audit trail
React components for managing terms in the UI:
Create ip_terms_audit table for:
Add expiresAt field for:
Add rate limiting to API endpoints to prevent abuse.
On This Page
Implementation StatusDirectory StructureAPI Route HandlersService LayerDatabase LayerTerms PackageImplementation PatternsAuthentication PatternAuthorization PatternError Handling PatternResponse PatternFormatting PatternDefault Generation PatternKey FunctionsformatIpTerms()resolveAccountId()getOrCreateIpTerms()generateIpTerms()approveIpTerms()deleteIpTerms()listIpTerms()Database Access FunctionsgetIpTermsById()getIpTermsByAccountAndType()listIpTermsByAccountId()createIpTerms()updateIpTerms()softDeleteIpTerms()Type SafetyType ConversionsTesting ConsiderationsUnit TestsIntegration TestsE2E TestsCommon Pitfalls1. Accessing Table Directly2. Forgetting Soft Delete Filter3. Using compensationValue Directly4. Not Setting updatedBy5. Hard Deleting RecordsFuture EnhancementsClient ComponentsAudit TableTerms ExpirationRate Limiting