Domain: Post
Scenario: Post Lifecycle (Create → Sync to Promoters → Checkout)
1. Business Goal
Post is the social commerce content unit that enables promoters (influencers) to resell products. A Post contains:
- Content: headline, body, media (images/videos)
- Products: relatedProducts with pricing rules (commission/wholesale)
- Permissions: what promoters can customize (price, media, etc.)
- Resale chain: original post → promoter posts (downstream)
2. Trigger & Entry
| API | Method | Description | Request Body |
|---|---|---|---|
/posts/curator |
POST | Create original post | { headline, bodyHtml, media[], relatedProducts[], financeMode } |
/posts/curator/:id |
PUT | Update original post | UpdateOriginalPostRequest |
/posts/promoter/create |
POST | Create resale post | CreateCuratorPostRequest |
/posts/curator/from-event |
POST | Create post from event | { eventId, ...postFields } |
/posts/curator/:postId/promoters/sync |
POST | Sync post to promoters | { promoterIds[] } |
/posts/curator/toggle-sync |
POST | Toggle event sync | { postId, syncEnabled } |
3. Data Flow (Sequence)
sequenceDiagram
participant Curator as Curator
participant API as PostsController
participant Service as PostsCuratorService
participant DB as PostgreSQL
participant Queue as BullMQ
participant PostSync as PostSyncAdapter
participant Downstream as DownstreamPosts
Curator->>API: POST /posts/curator
API->>Service: createPost(userId, dto)
Note over Service: 1. Generate urlAlias (unique)
Service->>DB: SELECT Post WHERE urlAlias = ?
DB-->>Service: null (unique check)
Note over Service: 2. Create Post record
Service->>DB: INSERT Post
DB-->>Service: post.id
Note over Service: 3. Create PostMedia records
Service->>DB: INSERT PostMedia[] (postId)
Note over Service: 4. Create PostTheme (if provided)
Service->>DB: INSERT PostTheme (postId)
Note over Service: 5. Handle relatedProducts
Service->>DB: UPDATE Post (relatedProducts = JSON)
Note over Service: 6. If event post, enable sync
alt createFromEventId present
Service->>DB: UPDATE Post (syncEnabled=true, createFromEventId)
Service->>PostSync: syncPostContent()
end
Note over Service: 7. Publish event
Service->>Queue: post.created
Queue->>Downstream: Trigger resale sync (if enabled)
Service-->>Curator: { id, status, urlAlias }
4. DB Operations (Chronological)
| Step | Table | Action | PK/FK Touched | Notes |
|---|---|---|---|---|
| 1 | Post |
INSERT | id (PK), creatorId (FK), parentPostId (FK for resale) |
status = ACTIVE, urlAlias unique |
| 2 | PostMedia |
INSERT | id (PK), postId (FK) |
images/videos |
| 3 | PostTheme |
INSERT | id (PK), postId (FK) |
theme customization |
| 4 | PostAccess |
INSERT (optional) | postId (FK) |
group-based access |
| 5 | PostSpecificAccess |
INSERT (optional) | postId (FK), userId (FK) |
user-specific access |
5. Event Post Sync Logic
When a Post is created from an Event (createFromEventId):
sequenceDiagram
participant Event as Event Module
participant Post as Post Module
participant Sync as PostSyncAdapter
participant Queue as POST_SYNC Queue
Event->>Post: Event updated (title, poster, etc.)
Post->>Queue: event.updated payload
Queue->>Sync: EventSyncTrigger
Note over Sync: Change Detection
Sync->>Sync: EventChangeDetector.detectChanges(oldEvent, newEvent)
Sync->>Sync: Determine syncCategories: metadata | variants
Note over Sync: Strategy Selection
alt metadata changed
Sync->>Sync: MetadataSyncStrategy
Sync->>Post: syncPostContent(post, {headline, title, media, ...})
end
alt variants changed
Sync->>Sync: VariantSyncStrategy
Sync->>Post: rebuildRelatedProducts(post, eventProducts)
end
Note over Sync: Propagate to Resale Posts
Sync->>Post: PostContentSyncHelper.syncToDownstream(postId)
Post->>Post: Update all posts where parentPostId OR originalPostId
Field Mapping (Event → Post):
// Direct copy
Event.title → Post.headline
Event.title → Post.title
Event.poster → Post.media
Event.poster → Post.coverImages
// Assembled
Event.venue + Event.startDate → Post.subTitle
Event.location + date → Post.customizeDisplayPrices
// bodyJson structure (Lexical)
{
type: "event-info-banner",
eventId: "event-id",
version: 1
}
6. Event/Queue Messages
| Message | Producer → Consumer | Payload Snippet |
|---|---|---|
post.created |
PostsModule → Queue | { postId, creatorId, createFromEventId } |
post.updated |
PostsModule → Queue | { postId, changes } |
post.deleted |
PostsModule → Queue | { postId } |
event.updated |
EventModule → POST_SYNC Queue | { oldEvent, newEvent, syncCategories } |
sync.trigger |
EventSyncTrigger → PostSyncAdapter | { postId, syncType, updates } |
7. Finance Modes
enum FinanceMode {
FINANCE_MODE_COMMISSION // Promoter earns % commission
FINANCE_MODE_WHOLESALE // Promoter sets wholesale price
}
Commission Mode:
// Promoter cannot change price, earns commission
relatedProducts: [{
merchantProductId,
variants: [{ promoterVariantId, commissionRate }],
syncWithCatalogPrice: true
}]
Wholesale Mode:
// Promoter sets their own price
relatedProducts: [{
merchantProductId,
variants: [{ promoterVariantId, wholesalePrice }],
canSetPrice: true
}]
8. File Map (Top 5 Must-Read)
| File | Purpose |
|---|---|
src/posts/curator/posts-curator.controller.ts |
Curator HTTP endpoints |
src/posts/curator/posts-curator.service.ts |
Core business logic |
src/posts/promoter/posts-promoter.service.ts |
Promoter view logic |
src/post-sync/post-sync-adapter.ts |
Sync orchestration |
src/post-sync/event-sync-trigger.ts |
Event → Post sync |
9. DB Schema Snippet (Prisma)
model Post {
id String @id @default(uuid()) @db.Uuid
creatorId String @map("creator_id") @db.Uuid
headline String
title String?
subTitle String @default("")
bodyHtml String
bodyJson String?
bodyText String @default("")
media Json? // [PostMediaItem]
coverImages Json?
// Status & lifecycle
status ProductStatus @default(ACTIVE)
publishedAt DateTime? @map("published_at")
deletedAt DateTime? @map("deleted_at")
urlAlias String @unique
// Metrics
numViewed Int @default(0)
numSold Int @default(0)
totalNumViewed Int @default(0)
totalNumSold Int @default(0)
// Relationships
parentPostId String? @map("parent_post_id") @db.Uuid
originalPostId String @map("original_post_id") @db.Uuid
// Event sync
createFromEventId String? @map("create_from_event_id") @db.Uuid
postType PostType @default(STANDARD)
syncEnabled Boolean @default(false)
lastSyncedAt DateTime? @map("last_synced_at")
// Finance
financeMode FinanceMode @default(FINANCE_MODE_COMMISSION)
financeModeForPromoters FinanceMode @default(FINANCE_MODE_COMMISSION)
// Resale permissions
allowPromotersResell Boolean @default(false)
allowPromotersSetPrice Boolean @default(false)
allowPromotersCustomizeMedia Boolean @default(false)
// Access control
isAccessRestricted Boolean @default(false)
allowPromotersAccess Boolean @default(false)
// Relations
PostMedia PostMedia[]
PostTheme PostTheme?
PostAccess PostAccess?
PostSpecificAccess PostSpecificAccess[]
parentPost Post? @relation("PostHierarchy", fields: [parentPostId], references: [id])
resalePosts Post[] @relation("PostHierarchy", inverse: [parentPost])
event Event? @relation(fields: [createFromEventId], references: [id])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
}
model PostMedia {
id String @id @default(uuid()) @db.Uuid
postId String @map("post_id") @db.Uuid
src String
type String
position Int
Post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@index([postId])
}
model PostTheme {
id String @id @default(uuid()) @db.Uuid
postId String @unique @map("post_id") @db.Uuid
primaryColor String?
backgroundColor String?
buttonColor String?
Post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
}
enum PostType {
STANDARD
EVENT
}
enum FinanceMode {
FINANCE_MODE_COMMISSION
FINANCE_MODE_WHOLESALE
}
10. Resale Chain
Original Post (curatorId = A)
├── Resale Post 1 (creatorId = B, parentPostId = original, originalPostId = original)
├── Resale Post 2 (creatorId = C, parentPostId = original, originalPostId = original)
│ ├── Sub-Resale 1 (creatorId = D, parentPostId = resale2, originalPostId = original)
│ └── Sub-Resale 2 (creatorId = E, parentPostId = resale2, originalPostId = original)
└── ...
// Pricing inheritance
Resale posts inherit financeMode from original
- Commission mode: earn % of sale
- Wholesale mode: set their own price