0

New Invoices

by
Published today

Trigger returning invoices created since the last run, tracked by a created-at watermark with id de-duplication.

Scriptยท trigger coupa Verified

The script

Submitted by hugo989 Typescript (fetch-only)
Verified 2 hours ago
1
//native
2

3
import * as wmill from "windmill-client"
4

5
type Invoice = { id: number; "created-at": string }
6

7
/**
8
 * New Invoices
9
 * Trigger returning invoices created since the last run, tracked by a created-at watermark with id de-duplication.
10
 */
11
export async function main(auth: RT.Coupa) {
12
  const base = auth.instance_url.replace(/\/+$/, "")
13
  const state: { watermark: string; seenIds: number[] } | null =
14
    await wmill.getState()
15

16
  if (!state || !state.watermark) {
17
    // First run: start 60s in the past (clock-skew tolerance), return no backlog
18
    const seed = new Date(Date.now() - 60_000)
19
      .toISOString()
20
      .replace("Z", "+00:00")
21
    await wmill.setState({ watermark: seed, seenIds: [] })
22
    return []
23
  }
24

25
  // gt_or_eq + id de-dup instead of gt, so invoices sharing the watermark second aren't skipped
26
  const returned: Invoice[] = []
27
  let offset = 0
28
  while (true) {
29
    const url = new URL(`${base}/api/invoices`)
30
    url.searchParams.append("created_at[gt_or_eq]", state.watermark)
31
    url.searchParams.append("limit", "50")
32
    url.searchParams.append("offset", String(offset))
33
    const response = await fetch(url, {
34
      headers: {
35
        Authorization: `Bearer ${auth.token}`,
36
        Accept: "application/json",
37
      },
38
    })
39
    if (!response.ok) {
40
      throw new Error(`${response.status} ${await response.text()}`)
41
    }
42
    const page = (await response.json()) as Invoice[]
43
    returned.push(...page)
44
    if (page.length < 50) break
45
    offset += 50
46
  }
47

48
  const seen = new Set(state.seenIds)
49
  const newInvoices = returned.filter((i) => !seen.has(i.id))
50

51
  if (newInvoices.length > 0) {
52
    // Advance the watermark to the newest Coupa-returned created-at; remember
53
    // the ids at that exact instant so the next gt_or_eq query can skip them
54
    const maxEpoch = Math.max(
55
      ...returned.map((i) => new Date(i["created-at"]).getTime())
56
    )
57
    const atMax = returned.filter(
58
      (i) => new Date(i["created-at"]).getTime() === maxEpoch
59
    )
60
    await wmill.setState({
61
      watermark: atMax[0]["created-at"],
62
      seenIds: atMax.map((i) => i.id),
63
    })
64
  }
65
  return newInvoices
66
}
67