# Đồng bộ creative

> **TL;DR** — Kéo các creative quảng cáo đã duyệt theo kiểu tăng dần: truyền checkpoint gần nhất của bạn vào `updated_since`, đi theo `next_cursor` tới hết, ghi đè bản ghi theo `creative_id`, và chỉ dời checkpoint khi cả lượt chạy đã thành công. Kiểm lại một creative đã lưu bằng `GET /creatives/{id}`; ngừng dùng nó sau ngày UTC của `rights.ends_at`.

## Trước khi bắt đầu
- Một key có `creatives:export` cho job, đặt trong `WANDURA_API_KEY`. Job marketing không bao giờ cần `playback`.
- Bản ghi và lớp vỏ response: [Creative](https://docs.wandura.ai/vi/developers/concepts/creatives).

## Quy trình
1. **Lần chạy đầu:** `GET /creatives?limit=100` không có `updated_since`; khi `data.next_cursor` còn khác `null`, gọi lại với `cursor=<next_cursor>`.
2. **Upsert** mọi bản ghi theo `creative_id`. Một bản ghi có thể đến hai lần (`updated_at` được làm tròn xuống tới giây, và `updated_since` là so sánh nghiêm ngặt): cứ ghi đè.
3. **Checkpoint:** sau khi lượt chạy **hoàn tất**, lưu `updated_at` lớn nhất bạn đã nhận.
4. **Các lần chạy sau:** làm như trên, với `updated_since=<checkpoint>` ở mọi trang của lượt chạy.
5. **Khi lỗi** (kể cả `422` `4222`, cursor không còn hợp lệ): giữ checkpoint cũ; lần chạy sau bắt đầu lại từ đó. Không bỏ sót gì.
6. **Creative bị gỡ** không được thông báo. Trước khi dùng một creative đã lưu, hoặc mỗi ngày một lần, gọi `GET /creatives/{id}`: `404` `4040` nghĩa là ngừng dùng nó.
7. **Bản quyền:** dùng được tới hết ngày UTC mà `rights.ends_at` trỏ tới (`ends_at + 86400`).

Vài lần chạy mỗi giờ vẫn nằm thoải mái trong rate limit của creative.

## Code
```ts
export async function syncCreatives(store: CreativeStore, o: CallOptions = {}): Promise<boolean> {
  const since = await store.checkpoint();
  let cursor: string | null = null;
  let max = since ?? 0;
  try {
    do {
      const qs = new URLSearchParams({ limit: '100' });
      if (since !== null) qs.set('updated_since', String(since)); // the same value on every page of a run
      if (cursor) qs.set('cursor', cursor);
      const { data }: { data: Envelope } = await call<Envelope>('GET', `/api/delivery/v1/creatives?${qs}`, { policy: BACKGROUND, ...o });
      for (const c of data.data.items) { await store.upsert(c); max = Math.max(max, c.updated_at); }
      cursor = data.data.next_cursor;
    } while (cursor);
  } catch (e) {
    // 422 / code 4222 (cursor no longer valid) or anything else: the next run restarts from the old checkpoint.
    log('creatives_sync_failed', { status: (e as { status?: number }).status, code: (e as { errorCode?: string }).errorCode });
    return false;
  }
  await store.saveCheckpoint(max); // only after the whole run: nothing is ever skipped
  return true;
}
```

## Xong khi
- [ ] Lần chạy thứ hai khi phía Wandura không có thay đổi thì không trả về gì mới.
- [ ] Lượt chạy bị ngắt giữa chừng không dời checkpoint.
- [ ] Creative đã quá ngày bản quyền không còn được dùng.

## Liên quan
- [Liệt kê creative](https://docs.wandura.ai/vi/developers/reference/creatives-list), [Lấy một creative](https://docs.wandura.ai/vi/developers/reference/creatives-get)
