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.titlePost.headline
Event.titlePost.title
Event.posterPost.media
Event.posterPost.coverImages

// Assembled
Event.venue + Event.startDatePost.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

results matching ""

    No results matching ""