15 Commits

Author SHA1 Message Date
f8ef7918ef fix(sales-summaries): stop days falling out of balance
Four faults were leaving restaurant days out of balance — one in the
data, three in the arithmetic. Measured over ninety days on a restored
copy of production (210 clients, 18,900 client-days): 1,258 days out of
balance and $69,560.10 becomes 123 days and $2,970.35, of which only 33
are above ten cents.

1,135 days repaired, none knocked out of balance, and not one
already-balanced day altered — verified line by line (category, side,
amount to the cent, account), not just on each day's bottom line.

THE DATA FAULT

Ten Square locations were configured against two client records each.
Sales orders scoped their identifier by client; refunds, card payments,
payouts and cash-drawer shifts used the bare Square id. Those attributes
are :db.unique/identity, so both clients' imports resolved to a single
entity and the last writer won — 3,387 refunds, 4,069 payouts and 2,628
cash-drawer shifts changed hands over time, across 19 client pairs of
which only 10 are visible in today's configuration.

Worse, one payment could belong to two orders. :sales-order/charges is
:db/isComponent, so removing a voided order cascaded into payments the
other client still needed.

Fixes: client-scope the four key schemes; look the record up under both
schemes so the change deploys before the migration finishes; and a
migration that gives every order its own payment. Run over the whole
database that is 19,040,785 orders walked, 9,100,314 payments re-keyed
and 200,027 copied, ending with 17,047,142 payments scoped, none left to
rename, none unscopable, and no payment owned by more than one order.
Idempotent and resumable; about thirteen minutes.

THE ARITHMETIC FAULTS

- Refunded tips stayed on the books. get-tip summed tips by joining
  through :sales-order/charges, so a return-only order — no tender to
  join through — contributed nothing while its reversal sat unread on
  :sales-order/tip. Additive, not substitutive: where an order does have
  a tender the tender is the correct source.

- Service charges were collected but never earned. Nothing read
  :sales-order/service-charge. Now credited for Square orders only, both
  signs, behind summary-service-charges.

- A refund on a day with no sales had nothing to offset it. Refunds are
  credited on the day the money goes back; the return that offsets them
  is read from that day's orders. get-returns now falls back to the day's
  refunded total, but only where the client recorded no sales orders at
  all — with no orders there is no order-derived return to double-count
  and no trading day can be moved. Behind summary-refund-only-returns.

Both flags are off by default, so deploying this changes nothing until a
client is opted in. docs/2026-08-15-sales-summary-rollout-plan.md has the
steps.

SUPPORTING

- Install schema attributes before the tuples that compose them. A tuple
  in schema.edn is built from an attribute in cloud-migration-schema.edn,
  so every test fixture died in setup — very likely why sales summaries
  had no tests before this.
- Log each day's imbalance and its suspect lines.
- Bound the dirty-summary scan to one client: 1,321 ms to 5.6 ms.
- compare-sales-summaries lives in test/clj as auto-ap.tools.* — it is a
  verification harness, not part of the running application. Its
  docstring now warns that d/as-of cannot be used to compare summary
  amounts: :ledger-mapped/amount, ledger-side and account are
  :db/noHistory, so a recomputed summary reads back with its amounts
  absent and looks like a legitimate balanced day.

28 tests, 65 assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:19:04 -07:00
943bc18842 update 2026-08-14 18:19:41 -07:00
8af9bfdee2 Merge branch 'feat/scheduled-sales-summary-refresh' 2026-08-12 16:10:44 -07:00
95d0537c69 feat(sales-summaries): refresh on a schedule, skipping accepted days
sales-summaries-v2 recomputes every dirty summary, but nothing set the dirty
flag on a schedule: mark-all-dirty was only ever called by hand from the
comment block, and the job was registered in neither server.clj's
INTEGREAT_JOB dispatch nor terraform/deploy.tf, so -main was dead code that
could never run in production. Summaries were only recalculated when someone
remembered to do it in the REPL, and POS data keeps arriving after a business
day closes, so a summary computed once on the day was routinely wrong and
stayed wrong.

Add a daily job that marks the trailing 7 days dirty and recomputes them,
leaving finished work alone. "Finished" is the condition the app already calls
Balanced -- debits equal credits and every line is mapped to an account. Since
that is derived rather than stored, a summary that later falls out of balance
is picked up again on the next run.

Extract the Balanced predicate into auto-ap.datomic.sales-summaries so the
grid's pill and the job share one definition, rather than a background job
requiring an SSR namespace. total-debits/total-credits resolve the ledger side
from either a plain keyword or the {:db/ident ...} map a pull returns, and
accepted? requires every item to declare a side: un-normalized pulled items
otherwise sum to 0.0 on both sides, read as balanced, and get skipped
silently and permanently.

Also fix sales-summaries-v2 destroying user-entered line items. It filtered
for :sales-summary-item/manual? to preserve them, but dirty-sales-summaries'
index-pull selector never fetched :sales-summary/items, so manual-items was
always empty. Because items is a component attribute upserted via
[:reset-rels ...], every recompute deleted the hand-entered lines -- often the
very lines that make a summary balance. Harmless while nothing ran on a
schedule; destructive the moment this does.

Register the job in the admin Background Jobs dropdown too, with a days
field: schedules are prod-only, so the admin page is the only way to run it
on staging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:17:54 -07:00
5126f3799d data(sysco): apply client category verification sheet to line-item mapping
Recategorizes 131 Sysco line-item descriptions per the client-reviewed
"Product Category Verification with codes" sheet: 96 existing rows re-coded
and 35 new rows appended (ids 1796-1830).

Root cause this addresses: get-line-account matches on exact description
string and silently defaults anything unmapped to 50000 Food Costs. Only 147
of the sheet's 321 reviewed rows coded the way the client expected. Note the
sheet's "change" column understates the work -- 20 of its 53 change rows are
no-ops (Paper -> Paper, confirming the gloves/liners/hairnets) and 3 were
already fixed in 38575aa5 / 7a0e256f, while 214,398 lines of movement come
from rows the client ticked as correct against a suggestion that already
differed from production.

Moves, replayed over all 1,022,732 DET lines in sysco-poller:

  50000 -> 51500 Dry Goods            90,481 ln   $5,215,011.82  105 clients
  50000 -> 51450 Dressing & Sauce     58,191 ln   $4,445,726.56   98
  50000 -> 51400 Bread and Bun        37,705 ln   $3,964,462.24   96
  50000 -> 52000 Soft Beverage        36,244 ln   $1,033,497.52   94
  50000 -> 51200 Produce               6,519 ln     $460,629.32   98
  55000 -> 51500 Dry Goods             6,516 ln     $259,750.60   97
  50000 -> 74100 Cleaning Supplies     5,630 ln     $205,233.19   98
  55000 -> 74100 Cleaning Supplies     5,222 ln     $125,946.32   99
  50000 -> 51120 Chicken/Poultry         265 ln      $42,482.50    8
  50000 -> 51300 Dairy                    36 ln       $5,941.69    6
  50000 -> 55000 Paperware                54 ln       $1,751.22   18
  54400 -> 51450 Dressing & Sauce         24 ln       $1,413.26    1
  total                               246,887 ln  $15,761,846.24

Only three source accounts are touched: 50000 and 55000 (the two silent
defaults) plus the single intended 54400 -> 51450 vinaigrette row. Nothing
else leaves a deliberately assigned account.

The 7 Misc Charges descriptions are deliberately left alone per Bryce,
including PICKLE CHIP KOSH 1/4 KK, which therefore stays at the 50000
default rather than moving to Produce as the sheet originally suggested.

Also corrects the PAPER & DISP fallback comment in sysco.clj: 440 of its 455
mapped descriptions point at 55000, not all 454. The 15 exceptions (foil
pans -> 51500, scour pads -> 74100) are mapped explicitly, so the
description map still wins ahead of the fallback. No logic changed.

Affects only clients with the code-sysco-items feature flag, and only at
import time -- already-imported invoices keep their existing splits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 22:00:57 -07:00
7a0e256f07 fix(sysco): fall back to PAPER & DISP category when description is unmapped
The Sysco importer codes each line item by exact description match against
resources/sysco_line_item_mapping.csv, silently defaulting to GL 50000 (Food
Costs) when the description is absent. Every new or renamed Sysco SKU
therefore leaks into Food Costs until someone hand-patches the CSV, which is
what 38575aa5 did for 34 descriptions.

Add a category-level fallback consulted after the description map and before
the 50000 default, enabled for PAPER & DISP only. The description mapping
still wins wherever it exists, so nothing already mapped changes.

PAPER & DISP is safe to generalize: all 454 mapped PAPER & DISP rows point at
55000, with no exceptions. Of the 852 distinct descriptions ever invoiced
under that category, only 3 resolved elsewhere, each because a row with a
different category shared the description and won the later-wins (into {}).
One of those, DESSERT CUP, was simply mis-categorized -- it is paper, and its
own lid (id 1782 LID DOME DESSERT CUP) was already 55000 -- so correct id 1772
to PAPER & DISP / 55000. The remaining two stay at 50000 on purpose, since
they are not paper: PAD SCRUB S-S 35 GRAM 1.25 OZ (SUPP & EQUIP) and TEST
STRIP SANITIZER QUAT (CHEMICAL/JANTRL).

Verified by replaying both changes over all 1,022,732 DET lines in the 56,010
CSVs under sysco-poller/: every resulting transition is 50000 -> 55000 (10,994
lines, $728,106.62). No line that already resolved to a non-default account
moved.

Note this only affects clients carrying the code-sysco-items feature flag, and
only on import -- already-imported invoices need a separate recode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 07:43:58 -07:00
2b5fbaca00 Add template for new Reel Produce statement layout
The statement no longer prints "Reel Produce" as text (only
orders@reelproduce.com), switched to MM/DD/YYYY dates, and moved the
invoice number into an "INV #..." transaction description, so no
template matched and the file fell through to the glimpse2 fallback.

Adds a QuickBooks-statement-style template (same shape as Suncrest /
Ocean Queen) keyed on reelproduce.com + Statement, placed after the
existing Reel Produce statement template so the old layout still wins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:24:31 -07:00
473556a45d Backfill script for olo 2026-07-29 09:59:02 -07:00
604d1ee1cf changes 2026-07-25 21:13:44 -07:00
e095cb94e4 fix(config): propagate rotated Plaid secret to worker configs
The rotation in 111eca41 updated the Plaid secret-key only in prod.edn,
leaving prod-background-worker.edn, prod-cloud-background-worker.edn, and
prod-cloud.edn on the old (now-invalidated) secret. Background worker jobs
loading those files failed with INVALID_API_KEYS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:29:15 -07:00
fa25620b7a olo fixes 2026-07-22 21:49:24 -07:00
111eca413e rotates passwords 2026-07-12 20:44:05 -07:00
fc54b92ddb fixes 2026-06-04 22:59:51 -07:00
019a1b4cd8 fixes 2026-06-04 22:56:01 -07:00
38575aa5bd data(sysco): add missing line-item GL mappings for paper & other items
The Sysco importer codes line items by exact-matching the item description
against resources/sysco_line_item_mapping.csv, falling back to GL 50000
(Food Costs) when no entry exists. On master, 8 of the paper-product
descriptions on recent invoices (e.g. BAG PAPER 250 CT, NAPKIN 2PLY INTR
FOLD 6.3X8.26, CONTAINER PAPER 4/110OZ NTG) were missing, so they
defaulted to 50000 instead of 55000 (Paper Costs).

Append the 34 curated mappings (Ids 1762-1795) covering these paper items
(-> 55000) plus the other new items from the same invoices, so they code
correctly on re-import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:28:23 -07:00
34 changed files with 3933 additions and 652 deletions

View File

@@ -173,4 +173,4 @@
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"}}
:plaid {:base-url "https://production.plaid.com"
:client-id "61bfab05f7e762001b323f79"
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}}
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}}

View File

@@ -31,5 +31,5 @@
:yodlee2-proxy-port 8888
:plaid {:base-url "https://production.plaid.com"
:client-id "61bfab05f7e762001b323f79"
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}
}

View File

@@ -34,5 +34,5 @@
:yodlee2-proxy-port 8888
:plaid {:base-url "https://production.plaid.com"
:client-id "61bfab05f7e762001b323f79"
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}
}

View File

@@ -6,7 +6,7 @@
:scheme "https"
:dd-env "prod"
:dd-service "integreat-app"
:jwt-secret "auto ap invoices are awesome"
:jwt-secret "rotated secrets are the best"
:invoice-import-queue-url "https://sqs.us-east-1.amazonaws.com/679918342773/integreat-mail-prod"
:requests-queue-url "https://sqs.us-east-1.amazonaws.com/679918342773/integreat-background-request-prod"
:invoice-email "invoices@mail.app.integreatconsult.com"
@@ -25,13 +25,13 @@
:run-background? false
:run-web? true
:yodlee2-integreat-user "integreat-main"
:yodlee2-client-id "3AATcwfPsWP1rP9oDoo4HvZhtaroGVcA"
:yodlee2-client-secret "cXTBmKbGfkaBFIpM"
:yodlee2-client-id "lNgzSdjkVXSkXyOBlResXUsCpCpBBDlG"
:yodlee2-client-secret "U3qjZP2gErZfbuTPud+LNJF9jHbNRzCWCZbEi6dDiHsziCNwI5yNNNrBAUsnjcu7VFsUVNmkxNKSW85Qf1YMOITC7q0kdv7MGv/ZqRLfQV5odkiPbLHgOrE7UE6//MtjU0jTznGA70WTPS+wwmugg8ArNnx+4QCHrrBrkRfFVOE="
:yodlee2-base-url "https://production.api.yodlee.com/ysl"
:yodlee2-fastlink "https://fl4.prod.yodlee.com/authenticate/USDevexProd2-319/fastlink/?channelAppName=usdevexprod2"
:yodlee2-proxy-host "172.31.10.83"
:yodlee2-proxy-port 8888
:plaid {:base-url "https://production.plaid.com"
:client-id "61bfab05f7e762001b323f79"
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}
}

View File

@@ -0,0 +1,115 @@
---
title: remove-voided-orders can delete another client's payments
type: risk
date: 2026-08-15
status: open — decide before merging the re-key
---
# `remove-voided-orders` can delete another client's payments
Measured on the restored backup, 2026-08-15. This risk is **pre-existing** — nothing in the
sales-summary work created it — but it is live right now, and the re-key work touches the same
data, so it should be understood before merging.
## The mechanism, in four steps
**1. Charges are component entities of an order.**
```clojure
;; resources/schema.edn
{:db/ident :sales-order/charges
:db/valueType :db.type/ref
:db/isComponent true ;; <- this is the load-bearing bit
:db/cardinality :db.cardinality/many}
```
`:db/isComponent true` tells Datomic the charges *belong to* the order. It is what lets you
transact an order with its tenders nested inside, and it means the charges have no independent
existence as far as Datomic is concerned.
**2. `retractEntity` on a component parent deletes the children too.**
That is the documented behaviour of `:db/retractEntity`: it recursively retracts component
values. `square.core3/remove-voided-orders` ends with exactly that:
```clojure
(s/map (fn [[o]]
[[:db/retractEntity [:sales-order/external-id (:sales-order/external-id o)]]]))
```
It asks Square for the last 10 days of orders, keeps the ones that should *not* be imported —
voided and cancelled orders — and retracts any of those we already stored. That is correct and
desirable on its own: a voided order should not sit in the books.
**3. But one charge can be shared by two orders.**
When two clients are configured on the same Square location, both import the same Square data.
Order keys embed the client, so each client gets its own order entity. Charge keys did **not**
embed the client, and `:charge/external-id` is `:db.unique/identity`, so both clients' orders
resolved to *the same charge entity*:
```
NGCD order 17592395490523 ──┐
├──> charge 17592490524 ← one entity, two parents
NGCC order 17592395511722 ──┘
```
**4. So retracting one order deletes a charge the other order still points at.**
Datomic sees a component and removes it. The surviving order keeps its line items — its sales —
but its tender is gone. The day then shows revenue with no payment against it, the summary goes
out of balance, and the payment is gone from the current database value. (History retains it, so
it is recoverable by someone who knows to look, but nothing in the app will show it again.)
## How exposed are we
Measured over the 10 contended clients across 2026-07-13 → 08-14:
| | |
|---|---|
| Charges examined | 56,829 |
| **Referenced by more than one order** | **35,870 (63%)** |
So this is not a theoretical corner. Roughly two thirds of the charges in that population have
two parents, and any voided order among them takes a charge down with it.
The exposure window for *new* damage is the rolling 10 days `remove-voided-orders` searches, but
the shared charges themselves span the whole period the locations were double-configured.
## What changes after Phase 0 and the re-key, and what doesn't
- **Phase 0 (done on the restore)** stops new sharing: only one client per location imports now,
so no new order pairs form.
- **The re-key (done for refunds and the contended clients' charges)** makes sharing structurally
impossible going forward, because a charge key now contains the client code.
- **Neither retroactively splits the 35,870 charges that are already shared.** They still have two
parents. Until they are split, `remove-voided-orders` remains capable of deleting a payment
belonging to the other client.
This is why plan §3.3 forbids retracting anything — including any historical cleanup of the
duplicate clients' data — until a verification query shows zero charges with more than one parent.
## Options, roughly in order of preference
1. **Split the shared charges, then let removal run normally.** Re-import the affected window now
that keys are client-scoped, so each client creates its own charge entity. This reuses the
import path rather than hand-constructing component entities. Verify with a query for charges
having more than one referencing order; it must reach zero.
2. **Guard the retraction.** Before retracting an order, check whether any of its charges are
referenced by another order; detach those (retract the `:sales-order/charges` ref rather than
the charge) and retract the rest. Small, contained change, and it makes the operation safe
regardless of what shape the data is in — worth doing on its own merits even after a split.
3. **Do nothing and accept it.** Only defensible once every location has a single client *and*
the historical shared charges are gone. Not true today.
## What I did about it during the validation run
I ran the import on the restore with `remove-voided-orders` **skipped**, and ran the other steps
(`upsert-locations`, `upsert`, `upsert-payouts`, `upsert-refunds`) normally. That kept the
validation faithful to how the import behaves without risking silent payment loss in the data
the measurements were about to be taken from.
**Nothing in the production system has been changed.** This note is about a risk that already
exists there.

View File

@@ -0,0 +1,693 @@
<title>Ninety-Day Reconciliation</title>
<style>
:root {
--paper: #F6F8F7; --card: #FFFFFF; --ink: #141F1D; --ink-soft: #4A5C58;
--ink-faint: #7C8D89; --rule: #DCE4E1; --accent: #0E5B57; --accent-soft: #E3EFED;
--good: #1A6B49; --bad: #A03B26; --warn: #8A6410;
--shadow: 0 1px 2px rgba(20,31,29,.06), 0 8px 24px rgba(20,31,29,.05);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--paper: #0E1615; --card: #151F1E; --ink: #E8EFED; --ink-soft: #A3B3AF;
--ink-faint: #74847F; --rule: #26332F; --accent: #5FBDB4; --accent-soft: #16302E;
--good: #5FBE8C; --bad: #E08A72; --warn: #D6AC55;
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.3);
}
}
:root[data-theme="dark"] {
--paper: #0E1615; --card: #151F1E; --ink: #E8EFED; --ink-soft: #A3B3AF;
--ink-faint: #74847F; --rule: #26332F; --accent: #5FBDB4; --accent-soft: #16302E;
--good: #5FBE8C; --bad: #E08A72; --warn: #D6AC55;
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.3);
}
* { box-sizing: border-box; }
body { background: var(--paper); color: var(--ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 16px; line-height: 1.6; margin: 0; padding: 0 20px 96px; }
.wrap { max-width: 940px; margin: 0 auto; }
.measure { max-width: 66ch; }
.num { font-variant-numeric: tabular-nums; }
.mono { font-family: ui-monospace, "SF Mono", "Cascadia Code", monospace; font-variant-numeric: tabular-nums; }
header.masthead { padding: 72px 0 40px; border-bottom: 2px solid var(--ink); display: flex; flex-direction: column; gap: 14px; }
.eyebrow { font-size: 12px; letter-spacing: .14em; text-transform: uppercase; color: var(--accent); font-weight: 600; }
h1 { font-family: Georgia, "Iowan Old Style", serif; font-size: clamp(34px, 5.4vw, 54px);
line-height: 1.08; font-weight: 600; letter-spacing: -.015em; margin: 0; text-wrap: balance; }
.standfirst { font-size: 19px; color: var(--ink-soft); margin: 0; max-width: 62ch; }
.meta { display: flex; flex-wrap: wrap; gap: 10px 28px; font-size: 13px; color: var(--ink-faint); padding-top: 6px; }
.meta b { color: var(--ink-soft); font-weight: 600; }
section { padding-top: 56px; display: flex; flex-direction: column; gap: 20px; }
h2 { font-family: Georgia, "Iowan Old Style", serif; font-size: 27px; font-weight: 600; letter-spacing: -.01em; margin: 0; text-wrap: balance; }
h3 { font-size: 14px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); font-weight: 700; margin: 0; }
h4 { font-size: 18px; font-weight: 650; margin: 0; letter-spacing: -.01em; }
p { margin: 0; }
.measure p + p { margin-top: 14px; }
.ledger { display: grid; grid-template-columns: 1fr auto 1fr; border: 1px solid var(--rule);
border-radius: 4px; background: var(--card); box-shadow: var(--shadow); overflow: hidden; }
.ledger > div { padding: 26px 28px; display: flex; flex-direction: column; gap: 6px; }
.ledger .arrow { justify-content: center; align-items: center; border-left: 1px solid var(--rule);
border-right: 1px solid var(--rule); color: var(--ink-faint); font-size: 22px; background: var(--accent-soft); }
.side-label { font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); font-weight: 600; }
.figure { font-size: clamp(28px, 4.4vw, 40px); font-weight: 650; line-height: 1.05; letter-spacing: -.02em; }
.figure.after { color: var(--good); }
.subfig { font-size: 14px; color: var(--ink-soft); }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; }
.stat { background: var(--card); border: 1px solid var(--rule); border-radius: 4px; padding: 18px 20px; display: flex; flex-direction: column; gap: 4px; }
.stat .k { font-size: 30px; font-weight: 650; letter-spacing: -.02em; line-height: 1; }
.stat .l { font-size: 13px; color: var(--ink-soft); }
.stat.zero .k { color: var(--good); }
.problem { border-left: 3px solid var(--accent); padding-left: 24px; display: flex; flex-direction: column; gap: 14px; }
.problem.two { border-left-color: var(--warn); }
.problem.three { border-left-color: var(--bad); }
.scroll { overflow-x: auto; border: 1px solid var(--rule); border-radius: 4px; background: var(--card); }
table { border-collapse: collapse; width: 100%; font-size: 14.5px; }
th, td { padding: 11px 16px; text-align: left; border-bottom: 1px solid var(--rule); white-space: nowrap; }
thead th { font-size: 11.5px; letter-spacing: .09em; text-transform: uppercase; color: var(--ink-faint); font-weight: 700; background: var(--accent-soft); }
tbody tr:last-child td { border-bottom: none; }
td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; }
tr.total td { font-weight: 650; background: var(--accent-soft); }
.good { color: var(--good); font-weight: 650; }
.bad { color: var(--bad); font-weight: 650; }
.dim { color: var(--ink-faint); }
.callout { background: var(--card); border: 1px solid var(--rule); border-left: 3px solid var(--accent);
border-radius: 4px; padding: 20px 24px; display: flex; flex-direction: column; gap: 10px; }
.callout.warn { border-left-color: var(--warn); }
.callout .h { font-weight: 650; }
code { font-family: ui-monospace, "SF Mono", monospace; font-size: .9em; background: var(--accent-soft); padding: 1px 5px; border-radius: 3px; }
pre { margin: 0; padding: 20px; font-size: 13px; line-height: 1.7; white-space: pre; font-family: ui-monospace, "SF Mono", monospace; }
footer { margin-top: 72px; padding-top: 24px; border-top: 1px solid var(--rule); font-size: 13px; color: var(--ink-faint); display: flex; flex-direction: column; gap: 8px; }
ul { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 8px; }
.tech { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--accent);
font-weight: 700; border: 1px solid var(--accent); border-radius: 3px; padding: 2px 7px; display: inline-block; }
</style>
<div class="wrap">
<header class="masthead">
<div class="eyebrow">Sales summaries · measured on a restored production backup</div>
<h1>Ninety-Day Reconciliation</h1>
<p class="standfirst">Four faults were leaving restaurant days out of balance — one in the data, three in the arithmetic. This is what they were, what they cost, and what fixing them is worth, measured by running the real job over ninety days of real trading, twice: once with the fixes off and once with them on.</p>
<div class="meta">
<span><b>Window</b> 2026-05-10 → 2026-08-07</span>
<span><b>Client-days</b> <span class="num">18,900</span></span>
<span><b>Clients</b> <span class="num">210</span></span>
<span><b>Nothing in production was changed</b></span>
</div>
</header>
<section>
<div class="ledger">
<div>
<span class="side-label">Today's calculation, ninety days re-run</span>
<span class="figure num">$69,560.10</span>
<span class="subfig"><span class="num">1,258</span> days out of balance · <span class="num">93.34%</span> clean</span>
</div>
<div class="arrow" aria-hidden="true"></div>
<div>
<span class="side-label">The same ninety days, fixes on</span>
<span class="figure after num">$2,970.35</span>
<span class="subfig"><span class="num">123</span> days out of balance · <span class="num">99.35%</span> clean</span>
</div>
</div>
<div class="stats">
<div class="stat"><span class="k num">1,135</span><span class="l">client-days brought into balance</span></div>
<div class="stat zero"><span class="k num">0</span><span class="l">days knocked out of balance</span></div>
<div class="stat"><span class="k num">95.7%</span><span class="l">of the variance removed</span></div>
<div class="stat zero"><span class="k num">0</span><span class="l">payments shared between two clients</span></div>
</div>
<div class="measure">
<p><strong>In one sentence:</strong> a day's sales summary should show the money taken and the money earned agreeing to the penny, and on roughly one trading day in eight it did not — because two clients were fighting over the same records, tips that had been refunded were still counted as income, service charges customers paid were credited to nothing, and a refund that arrived on a day with no sales had nothing to offset it.</p>
<p><strong>How the two figures above were produced.</strong> Both are the real nightly job, run over the same ninety days against the same restored database, writing real summaries each time — the first pass with the fixes switched off, the second with them on. Comparing a re-run against a re-run rather than against production's stored summaries is the stricter test: production's figures are in places months stale, and crediting the fixes with repairing ordinary staleness would flatter them. On that fairer footing the fixes are worth <strong>1,135 days and $66,589.75</strong>, not the larger number a stale baseline would have shown.</p>
</div>
</section>
<section>
<h2>The four problems</h2>
<div class="problem">
<h4>1. Two client records sharing one Square location</h4>
<div class="measure">
<p><strong>For the business:</strong> ten restaurant locations were set up twice in the system, as two separate clients. Both were importing from Square. Because the two records competed for the same payments and refunds, a refund would belong to one client for twenty minutes, then the other — so a day's books could gain or lose a refund depending on nothing but timing. On 2026-07-23 one client's summary was missing a $71.94 refund entirely, and was out of balance by exactly that amount.</p>
<p><span class="tech">technical</span> Sales orders scoped their identifier by client (<code>square/order/&lt;code&gt;-&lt;loc&gt;-&lt;id&gt;</code>), but refunds, card charges, payouts and cash-drawer shifts did not — they used the bare Square id. Those attributes are <code>:db.unique/identity</code>, so both clients' imports resolved to a single entity and the last writer won.</p>
<p>Reading ownership out of the database's own history, this had actually happened to <strong>3,387 refunds, 4,069 payouts and 2,628 cash-drawer shifts</strong>. And it has involved <strong>19 client pairs, of which only 10 are visible in today's configuration</strong> — nine more contended in the past and the configuration has since changed, so no point-in-time check would find them.</p>
</div>
</div>
<div class="problem two">
<h4>2. One payment record owned by two orders</h4>
<div class="measure">
<p><strong>For the business:</strong> the same collision meant a single card payment could be attached to both clients' copies of an order. That is worse than untidy. The nightly import removes orders Square reports as voided, and removing an order also removes its payments — so cancelling one client's order could silently delete the <em>other</em> client's payment, leaving a day showing sales with no money against them.</p>
<p><span class="tech">technical</span> <code>:sales-order/charges</code> is declared <code>:db/isComponent true</code>, so <code>[:db/retractEntity &lt;order&gt;]</code> cascades into the charges. In a 20,000-order sample of the affected clients, <strong>11,469 charges had two parent orders</strong>. This is why <code>remove-voided-orders</code> was left switched off during testing.</p>
</div>
</div>
<div class="problem three">
<h4>3. Tips refunded, and service charges credited nowhere</h4>
<div class="measure">
<p><strong>For the business:</strong> two arithmetic faults, both of which overstated or understated a day.</p>
<ul>
<li><strong>Refunded tips stayed on the books.</strong> When a guest was refunded, the tip came back too — but the summary still counted the original tip as income. On one NGLK day the books credited $482.94 of tips beside a $60.00 refund of that very tip.</li>
<li><strong>Service charges were collected but never earned.</strong> A catering or auto-gratuity charge is inside the card payment the customer makes, so it arrived as money taken — but no line recorded it as money earned. One NTPT order carried $427.10 that was credited to nothing at all; the largest single instance was <strong>$1,344.86</strong> in one day.</li>
</ul>
<p><span class="tech">technical</span> <code>get-tip</code> summed tips by joining through <code>:sales-order/charges</code>, so a return-only order — which has no tender to join through — contributed nothing, while its reversal sat unread on <code>:sales-order/tip</code>. Nothing at all read <code>:sales-order/service-charge</code>.</p>
</div>
</div>
<div class="problem">
<h4>4. A refund on a day the restaurant did not sell anything</h4>
<div class="measure">
<p><strong>For the business:</strong> a refund is recorded on the day the money goes back to the guest. The matching reduction in sales is read from that day's orders. When both fall on the same day they cancel and the day balances. When they do not — a refund settling after a closed day, or a retired duplicate record that still receives refunds while its orders belong to the surviving record — the day is left carrying a refund with nothing against it, and is out by exactly the refunded amount.</p>
<p><strong>This is the answer to why the duplicated restaurants looked so much worse than everyone else.</strong> Of the days still failing after the first three fixes, <strong>155 of the 158 on duplicate records had no sales orders at all</strong> — the summary consisted of nothing but orphaned refunds and their fees. The refunds were claimed by the record that happened to import them during the shared period; the orders went to the other record. Nothing was double-counted and no money was lost — the two halves of each refund simply ended up on two different sets of books.</p>
<p>It is not a duplicate-only problem, which is what makes it worth fixing properly rather than papering over. The single largest non-duplicate cluster, <span class="mono">NG4S</span>, has the identical shape: no orders on any day of the window, refunds arriving anyway, and every one of those days out by exactly the refund.</p>
<p><span class="tech">technical</span> <code>get-returns</code> summed <code>:sales-order/returns</code> over orders scanned for the date. With no orders the sum is nil and no <code>Returns</code> line is written at all, while <code>get-refund-items</code> still credits <code>Card Refunds</code> from the <code>sales-refund</code> records — a credit with no debit anywhere in the day.</p>
</div>
</div>
</section>
<section>
<h2>What the fixes actually are</h2>
<div class="measure">
<p>Six changes. The first three stop two clients from sharing a record; the last three record
money that was being collected, or handed back, but not booked. Each is small — the difficulty
was knowing which line to change, not writing it.</p>
</div>
<h3>1 · Put the client in the record's name</h3>
<div class="measure">
<p>Every imported record has an identifier the importer uses to decide "have I seen this
before?". Sales orders already included the client; refunds, card payments, payouts and
cash-drawer shifts did not, which is precisely why two clients could land on one record.</p>
</div>
<div class="scroll">
<pre><span class="dim">;; before — the bare Square id, identical for both clients</span>
(str "square/refund/" (:id r)) <span class="dim">;; square/refund/NOkQOTIiJULWN6…</span>
<span class="dim">;; after</span>
(scoped-key "square/refund/" client location (:id r))
<span class="dim">;; square/refund/NGCD-CD-NOkQOTIiJULWN6…</span>
(defn scoped-key [prefix client location id]
(str prefix (:client/code client) "-"
(:square-location/client-location location) "-" id))</pre>
</div>
<div class="measure">
<p>Applied at five places in the Square importer: order payments, refunds, payouts (twice —
the record itself and the lookup that finds it) and cash-drawer shifts. ezCater orders
already did this and needed no change.</p>
</div>
<h3>2 · Find the existing record before writing, under either name</h3>
<div class="measure">
<p>This is the one that makes the change safe to deploy. The identifiers are unique keys, so
the importer relies on "same id, same record". Rename them and the next import matches
nothing — and would quietly create a <em>second</em> copy of every refund and payment in the
system, leaving the originals orphaned. So the importer looks up the record explicitly,
new name first, old name second, and writes to whichever it finds.</p>
</div>
<div class="scroll">
<pre>(defn existing-id [db attr prefix client location id]
(when id
(or (dc/entid db [attr (scoped-key prefix client location id)]) <span class="dim">;; new scheme</span>
(dc/entid db [attr (str prefix id)])))) <span class="dim">;; legacy scheme</span></pre>
</div>
<div class="measure">
<p>The result is pinned as the record's id on the way in, so the write lands on the existing
row regardless of which name it currently carries. The proof this worked is a count that did
not move. Every one of the 265,965 refunds, payouts and cash-drawer shifts in the database was
re-named, and afterwards there were still exactly <strong>144,688 payouts and 69,291 cash-drawer
shifts</strong> — the same figures as at the restore point. Refunds went from 50,986 to 51,986,
and all 1,000 of those came from the live Square import run afterwards, not from the renaming.
Had the fallback lookup been missing, each of these would have doubled instead.</p>
</div>
<h3>3 · Give every order its own payment record</h3>
<div class="measure">
<p>Renaming stops <em>new</em> collisions but does not undo old ones: a payment already shared
by two orders is still one row with two owners. The migration walks each order's payments and,
where another order has already claimed one, makes that order its own copy with the same
amounts and points the order at the copy.</p>
</div>
<div class="scroll">
<pre><span class="dim">;; for each order, for each of its payments:</span>
:keep <span class="dim"></span> first order to claim it; rename in place
:clone <span class="dim"></span> copy type, total, tip, tax, date, processor, note, receipt link
set the copy's client and location to this order's
retract this order's link to the shared payment
link it to the copy instead</pre>
</div>
<div class="measure">
<p>Run over the whole database that was <strong>9,100,314 renamed and 200,027 copied</strong>,
and payments owned by two orders went from 11,469 in a 20,000-order sample to zero across
400,000 orders checked. The record count rose by exactly 200,027 — the number of copies it
reported making, which is the check that it created what it meant to and nothing else.</p>
<p>One subtlety worth recording, because it bit us: the Square id has to be recovered from the
record's current owner rather than by trimming a fixed prefix. Client codes contain dashes —
<span class="mono">N-30003</span> — so a pattern cannot tell where the client name ends and the
Square id begins. Getting this wrong scoped some records twice and doubled their tender.</p>
</div>
<h3>4 · Count tips that were handed back</h3>
<div class="measure">
<p>Tips were summed by walking from the order to its payments. A refund-only order has no
payment attached, so its negative tip was invisible. The fix adds those tips rather than
replacing the calculation.</p>
</div>
<div class="scroll">
<pre><span class="dim">;; before</span>
:ledger-mapped/amount (tendered-tip c date)
<span class="dim">;; after</span>
:ledger-mapped/amount (+ (tendered-tip c date)
(untendered-tip c date))
<span class="dim">;; untendered-tip — tips on orders with no payment attached</span>
[?e :sales-order/tip ?tip]
(not [?e :sales-order/charges])</pre>
</div>
<div class="measure">
<p><strong>Adding rather than replacing is deliberate.</strong> Where an order does have a
payment, the payment is the correct source: real orders exist whose payment carries a tip the
order does not — an auto-gratuity recorded as a service charge, or a wallet tip missing from
the order totals. Reading the order instead would have dropped those. Three tests hold this
in place: the refund case must change, and the tendered and ordinary cases must not.</p>
</div>
<h3>5 · Credit Square service charges, both signs</h3>
<div class="measure">
<p>Nothing read the service-charge field at all. A new line credits it, for Square orders only
and for negative amounts as well as positive.</p>
</div>
<div class="scroll">
<pre>[?e :sales-order/service-charge ?service-charge]
(or-join [?e]
[?e :sales-order/vendor :vendor/ccp-square]
(and (not [?e :sales-order/vendor])
[?e :sales-order/external-id ?external-id]
[(clojure.string/starts-with? ?external-id "square/order/")]))</pre>
</div>
<div class="measure">
<p><strong>Why the vendor test has two branches.</strong> ezCater service charges are commission
the platform deducts from the restaurant, not money the diner hands over, so crediting them
would make a day worse rather than better — hence the Square-only condition. But whole eras of
Square orders carry no vendor field at all, and a test on vendor alone would silently credit
nothing. The second branch falls back to the order's own identifier.</p>
<p><strong>Why negatives matter.</strong> A returned catering fee arrives as a negative service
charge and is already deducted from the day's returns; dropping negatives would lose the
reversal. The line sits behind a per-client switch, off by default, so it can be turned on a
few restaurants at a time.</p>
</div>
<h3>6 · Recognise a return on a day that refunded but did not sell</h3>
<div class="measure">
<p>Where a day has refunds and no sales orders whatsoever, book a <code>Returns</code> debit
equal to that day's refunds. The guard — <em>no orders at all</em> — is what makes this
mechanical rather than a judgement call.</p>
</div>
<div class="scroll">
<pre><span class="dim">;; the return a day owes when it refunded but never sold</span>
(defn- refund-only-returns [c date]
(when (and (refund-only-returns-enabled? c)
(not (traded? c date)))
(let [amount (refunded-total c date)]
(when-not (zero? amount) amount))))
<span class="dim">;; get-returns falls back to it only when the order-derived figure is absent</span>
(or (ffirst (dc/q <span class="dim">;; sum of :sales-order/returns, as before</span>))
(refund-only-returns c date))</pre>
</div>
<div class="measure">
<p><strong>Why the guard is the whole design.</strong> On a day with no orders the
order-derived return is necessarily nothing, so there is no figure to double-count, and no
trading day can have its numbers moved by this rule — which the measurement bears out: 156
days came into balance, none went out, and not one already-balanced day had a line change.
Days that <em>did</em> trade and still carry an unmatched refund are deliberately left alone;
apportioning a return across a day that also sold is an accounting question, not a mechanical
one, and guessing at it would be exactly the kind of change that quietly rewrites correct
books.</p>
<p>Like the service-charge line, it sits behind a per-client switch that is off by default.
Four tests hold it: flag off changes nothing; a refund-only day balances exactly; a day that
traded keeps its own return; and a day that traded but returned nothing does not pick up the
refunded total either — that last one guards the difference between "the client did not
trade" and "the order-derived figure happened to be nil", which is the subtle way this rule
could have gone wrong.</p>
</div>
<h3>Supporting changes</h3>
<div class="scroll">
<table>
<thead><tr><th>Change</th><th>Why</th></tr></thead>
<tbody>
<tr><td>Log each day's imbalance and its suspect lines</td><td>an out-of-balance day was only visible by opening the screen; now it can be queried</td></tr>
<tr><td>Stop the dirty-summary scan at the client boundary</td><td>it read every later client's summaries too — 1,321 ms to 5.6 ms per client</td></tr>
<tr><td>Split the recompute driver into a per-client function</td><td>lets a backfill spread clients across threads instead of grinding one at a time</td></tr>
<tr><td>Install schema attributes before the tuples that compose them</td><td>the test suite could not build an empty database at all, so no test could run</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p>That last one is worth a sentence for engineers: <code>transact-schema</code> installed
schema.edn then cloud-migration-schema.edn, but a composite tuple in the first file is built
from an attribute in the second. Datomic will not create a tuple before its members exist, so
every test fixture died in setup. It is very likely why sales summaries had no tests before
this work.</p>
</div>
</section>
<section>
<h2>What each fix is worth</h2>
<div class="measure">
<p>The job was run over the same ninety days at each stage, writing real summaries every time, so these are measured outcomes rather than estimates. All 18,900 client-day summaries in the window are included, whether or not the restaurant traded that day.</p>
</div>
<div class="scroll">
<table>
<thead><tr><th>Stage</th><th class="n">Days out of balance</th><th class="n">Clean</th><th class="n">Total variance</th></tr></thead>
<tbody>
<tr><td>Today's calculation, ninety days re-run</td><td class="n">1,258</td><td class="n">93.34%</td><td class="n">$69,560.10</td></tr>
<tr><td>+ refunded tips</td><td class="n">971</td><td class="n">94.86%</td><td class="n">$66,414.39</td></tr>
<tr><td>+ service charges</td><td class="n">279</td><td class="n">98.52%</td><td class="n">$7,790.54</td></tr>
<tr class="total"><td>+ returns on refund-only days</td><td class="n good">123</td><td class="n good">99.35%</td><td class="n good">$2,970.35</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p><strong>Deduplication is not a row in this table, and that is deliberate.</strong> Separating the shared records is a change to the data, not to the arithmetic, and it had already been carried out before either pass ran — so both the baseline and the result above are computed on repaired data, and neither is credited with it. Its effect is shown structurally instead, further down: payments owned by two clients went to zero and stayed there. The consequence for reading this table is that <strong>$66,589.75 is what the three arithmetic fixes are worth on their own</strong>, with the deduplication's contribution already banked in the starting figure rather than added to the improvement.</p>
</div>
<h3>Day-by-day effect of each change</h3>
<div class="scroll">
<table>
<thead><tr><th>Change</th><th class="n">Unchanged</th><th class="n">Into balance</th><th class="n">Out of balance</th><th class="n">Balanced days altered</th><th class="n">Money moved</th></tr></thead>
<tbody>
<tr><td>Refunded tips</td><td class="n">18,590</td><td class="n good">287</td><td class="n good">0</td><td class="n good">0</td><td class="n">$3,712.67</td></tr>
<tr><td>Service charges</td><td class="n">18,208</td><td class="n good">692</td><td class="n good">0</td><td class="n good">0</td><td class="n">$58,349.85</td></tr>
<tr><td>Returns on refund-only days</td><td class="n">18,740</td><td class="n good">156</td><td class="n good">0</td><td class="n good">0</td><td class="n">$4,820.19</td></tr>
<tr class="total"><td>All three, end to end</td><td class="n">17,765</td><td class="n good">1,135</td><td class="n good">0</td><td class="n good">0</td><td class="n">$66,589.75</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p><strong>None of the three touched a day that was already correct.</strong> Across all 18,900 client-days, no balanced day was knocked out of balance, and no balanced day had a single figure altered. Every day that moved was already wrong. Service charges are by far the largest of the three, moving $58,349.85 against $4,820.19 for refund-only returns and $3,712.67 for tips.</p>
<p>That claim is stronger than a balance check, and it is the one worth insisting on: a day can stay balanced while its individual lines move, which would still be a change to the books. Every line of every summary was compared — category, debit or credit side, amount to the cent, and account — not just the day's bottom line.</p>
<p><strong>The arithmetic fixes account for their repair exactly.</strong> Adding up the untendered-tip and service-charge amounts for the 979 days those two closed leaves a residue of <span class="mono">0.0000000013</span> against $61,769.56 of imbalance removed. Nothing else moved those days; there is no unexplained remainder hiding a further effect.</p>
</div>
</section>
<section>
<h2>What it looks like on the page</h2>
<div class="measure">
<p>Both arithmetic fixes add exactly one credit line. Nothing else in a summary moves — no sales figure, no payment, no tax.</p>
</div>
<h3>A refunded tip — NGLK, 2026-08-04</h3>
<div class="scroll">
<table>
<thead><tr><th>Line</th><th class="n">Before</th><th class="n">After</th></tr></thead>
<tbody>
<tr><td><strong>Tip</strong></td><td class="n">482.94</td><td class="n">422.94</td></tr>
<tr><td class="dim">Card Refunds</td><td class="n dim">60.00</td><td class="n dim">60.00</td></tr>
<tr><td>Total money taken</td><td class="n">10,094.81</td><td class="n">10,094.81</td></tr>
<tr><td>Total money earned</td><td class="n">10,154.81</td><td class="n">10,094.81</td></tr>
<tr class="total"><td>Out of balance by</td><td class="n bad">60.00</td><td class="n good">0.00</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p>The day already carried a $60.00 card refund — the guest was given their money back, tip included — while the tip line still credited the full $482.94. The corrected figure matches the refund to the penny. The order behind it is <span class="mono">square/order/NGLK-SM-OxSX9gpXJV394qqT8mnBGypUwKNZY</span>: a tip of 60.00 on an order with no payment attached at all.</p>
</div>
<h3>A service charge — NTPT, 2026-08-06</h3>
<div class="scroll">
<table>
<thead><tr><th>Line</th><th class="n">Before</th><th class="n">After</th></tr></thead>
<tbody>
<tr><td><strong>Service Charges</strong></td><td class="n bad">not shown</td><td class="n">427.10</td></tr>
<tr><td class="dim">Card Payments</td><td class="n dim">4,975.89</td><td class="n dim">4,975.89</td></tr>
<tr><td>Total money taken</td><td class="n">7,777.20</td><td class="n">7,777.20</td></tr>
<tr><td>Total money earned</td><td class="n">7,350.10</td><td class="n">7,777.20</td></tr>
<tr class="total"><td>Out of balance by</td><td class="n bad">+427.10</td><td class="n good">0.00</td></tr>
</tbody>
</table>
</div>
<h3>The largest repairs of each kind</h3>
<div class="scroll">
<table>
<thead><tr><th>Client</th><th>Date</th><th>Line</th><th class="n">Before</th><th class="n">After</th><th class="n">Day closed</th></tr></thead>
<tbody>
<tr><td class="mono">NGPA</td><td>2026-06-04</td><td>Service Charges</td><td class="n bad">not shown</td><td class="n">1,344.86</td><td class="n good">+1,344.86 → 0</td></tr>
<tr><td class="mono">NTPT</td><td>2026-08-06</td><td>Service Charges</td><td class="n bad">not shown</td><td class="n">427.10</td><td class="n good">+427.10 → 0</td></tr>
<tr><td class="mono">N-30003</td><td>2026-05-27</td><td>Service Charges</td><td class="n bad">not shown</td><td class="n">405.83</td><td class="n good">+405.83 → 0</td></tr>
<tr><td class="mono">NGFL</td><td>2026-05-19</td><td>Tip</td><td class="n">238.46</td><td class="n">70.42</td><td class="n good">168.04 → 0</td></tr>
<tr><td class="mono">NGMI</td><td>2026-07-09</td><td>Tip</td><td class="n">230.01</td><td class="n">80.01</td><td class="n good">150.00 → 0</td></tr>
<tr><td class="mono">NGVA</td><td>2026-07-03</td><td>Tip</td><td class="n">152.66</td><td class="n">40.12</td><td class="n good">112.54 → 0</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p>In every case the correction equals the imbalance exactly, which is what you would expect if the fix is recording something real that was recorded nowhere. On NGNP 2026-06-25 both fixes land on one day and pull opposite ways — $301.40 credited, $1.80 removed, $299.60 closed — a useful check that they are independent.</p>
</div>
</section>
<section>
<h2>What was done to the data, and how it was checked</h2>
<div class="measure">
<p>Every step below was performed against a restored copy of the production database. Production itself was never touched.</p>
</div>
<div class="scroll">
<table>
<thead><tr><th>Step</th><th>Result</th></tr></thead>
<tbody>
<tr><td>Deactivate the duplicate client at each shared location</td><td class="n">10 locations · shared locations remaining: <span class="good">0</span></td></tr>
<tr><td>Walk every order in the database</td><td class="n">19,040,785 orders</td></tr>
<tr><td>Give every order its own payment record</td><td class="n">9,100,314 re-keyed · 200,027 copied</td></tr>
<tr><td><strong>Payments owned by two orders</strong></td><td class="n good">0 <span class="dim">across 400,000 orders checked</span></td></tr>
<tr><td>Client-scope refunds, payouts and cash-drawer shifts</td><td class="n good">counts unchanged · 0 collisions</td></tr>
<tr><td>Live Square import afterwards</td><td class="n good">0 orders with duplicated payment · 0 shared payments</td></tr>
<tr><td>Ownership changes after the change</td><td class="n good">0 refunds · 0 payouts · 0 shifts</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p>The count checks are the ones that matter. If re-keying had gone wrong it would have created a second copy of every record rather than updating the existing one, and the totals would have doubled. They did not move. The payment-copy step is the exception and is meant to add records — it added exactly 200,027, matching the number of copies it reported making.</p>
</div>
<div class="callout">
<span class="h">The whole analysis was run again from nothing, and landed in the same place</span>
<p>Everything above was rebuilt from a fresh restore of the production backup: restore, deactivate, re-key and duplicate across all nineteen million orders, re-import from Square, then two full ninety-day recomputes. At the point the two runs are comparable — before the refund-only fix below, which the earlier run did not have — the end state matched <strong>to the cent</strong>: 279 days out of balance and $7,790.54, with not one client-day differing by so much as half a penny. The recompute was then run a third time, after the database-wide re-key had finished, and produced the identical figure again. Numbers that survive being derived twice from separate restores are not artefacts of how the measurement was set up.</p>
</div>
<div class="callout warn">
<span class="h">A bug in this work, found by measuring rather than reading</span>
<p>The first attempt at copying shared payments derived each payment's Square identifier by stripping a fixed prefix. That is right the first time a payment is seen, but once it has been re-keyed to one client, a second order meeting it later read the already-scoped key as the identifier and scoped it twice — <span class="mono">NGCD-CD-NGCC-CC-&lt;id&gt;</span>. The importer then created a fresh payment, doubling the tender on five clients by $3,000$7,000 each. It was caught because the totals were absurd, not because the code looked wrong. The fix recovers the scope from the record itself; client codes contain dashes, so it cannot be done by pattern. A test now runs the step one order at a time, which is the arrangement that exposes it.</p>
</div>
</section>
<section>
<h2>What is still out of balance</h2>
<div class="measure">
<p>123 client-days out of 18,900, totalling <strong>$2,970.35</strong> — and only 33 of those are above ten cents. Where it sits matters more than the total.</p>
</div>
<div class="scroll">
<table>
<thead><tr><th>Where the remainder sits</th><th class="n">Days</th><th class="n">Variance</th></tr></thead>
<tbody>
<tr><td>The twenty client records tied to a shared location — including the ten now-dormant duplicates</td><td class="n">20</td><td class="n">$2,467.39</td></tr>
<tr class="total"><td>Every other client</td><td class="n">103</td><td class="n">$502.96</td></tr>
</tbody>
</table>
</div>
<div class="scroll">
<table>
<thead><tr><th>Client</th><th class="n">Days</th><th class="n">Variance</th><th>What it is</th></tr></thead>
<tbody>
<tr><td class="mono">NGDL</td><td class="n">3</td><td class="n">$859.57</td><td>order on one record, its payment on the twin</td></tr>
<tr><td class="mono">NGDU</td><td class="n">3</td><td class="n">$345.96</td><td>payout fees on days with no trading</td></tr>
<tr><td class="mono">NGBR</td><td class="n">1</td><td class="n">$299.42</td><td rowspan="2">Square recorded $6,358.99 of tender against $6,059.57 of order totals on 2026-08-06 — the gap itself, not a summary fault</td></tr>
<tr><td class="mono">NGBK</td><td class="n">1</td><td class="n">$299.42</td></tr>
<tr><td class="mono">NGMV</td><td class="n">5</td><td class="n">$259.38</td><td>late May, undiagnosed</td></tr>
<tr><td class="mono">NGEB</td><td class="n">4</td><td class="n">$199.09</td><td>ezCater fee treatment — an open question</td></tr>
<tr><td class="mono">NGVC</td><td class="n">3</td><td class="n">$173.81</td><td>payout fees on days with no trading</td></tr>
<tr><td class="mono">NGWN</td><td class="n">3</td><td class="n">$170.28</td><td>payout fees on days with no trading</td></tr>
<tr class="total"><td class="dim">everyone else</td><td class="n dim">100</td><td class="n dim">$363.42</td><td class="dim">90 of these are till rounding — pennies a day</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p><strong>What is left divides cleanly in two.</strong> 108 days totalling $1,995.36 are real trading days with genuine discrepancies — the NGBR/NGBK tender gap, the ezCater fee question, and the unexplained clusters. The other 15 days, $974.99, carry a processing-fee debit and nothing else: a payout fee landing on a day the restaurant did not trade. That is the same shape as the refund-only day and could be closed the same way, but the offsetting entry belongs to the payout rather than to sales, so it needs the payout side modelled rather than another rule in the summary. It is not worth holding this work for.</p>
<p>The clusters on NGMV and NGEB are unexplained and worth a look, though at $458 across nine days they are no longer urgent. They are visible only because the window is ninety days; a thirty-day view does not reach them.</p>
</div>
</section>
<section>
<h2>How complete is this</h2>
<div class="measure">
<p>The importer understands both the old and new record names, so the change can be deployed
before the renaming finishes. That tolerance is a bridge, not a destination — while any
record still carries an unscoped name, two clients can land on it and the guarantee rests on
a convention rather than on the data. So the renaming was run to completion and measured.</p>
</div>
<div class="scroll">
<table>
<thead><tr><th>Record type</th><th class="n">Total</th><th class="n">Client-scoped</th><th class="n">Still to rename</th><th class="n">Cannot be scoped</th></tr></thead>
<tbody>
<tr><td>Card payments</td><td class="n">17,047,142</td><td class="n good">17,047,142</td><td class="n good">0</td><td class="n good">0</td></tr>
<tr><td>Refunds</td><td class="n">51,986</td><td class="n good">51,986</td><td class="n good">0</td><td class="n good">0</td></tr>
<tr><td>Payouts</td><td class="n">144,688</td><td class="n good">144,652</td><td class="n good">0</td><td class="n">36</td></tr>
<tr><td>Cash-drawer shifts</td><td class="n">69,291</td><td class="n good">69,291</td><td class="n good">0</td><td class="n good">0</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p>Every record in the database now carries its owner's name, and the migration proposes no
further changes: asked what is left to do, it answers zero on all four record types. The 36
payouts are ones with no client or location recorded anywhere, on the record itself or on
anything referring to it, so there is nothing to name them after.</p>
<p>Renaming had to be driven from orders, because a payment's rightful owner is whichever
order refers to it — so completing it meant walking all <strong>19,040,785 orders</strong>, not
just the clients that look shared today. Nine client pairs contended in the past without
sharing a location now, and a migration scoped to the current configuration would have missed
every one of them.</p>
<p><strong>A caution about how completeness is counted.</strong> 282,649 card payments carry no
client attribute of their own — they are stubs the payout path creates, never referenced by an
order. A gate that checks the attribute reports these as "no owner" and looks like a gap. They
are not: their names are scoped, recovered from the deposit that holds them. The figures above
are counted the harder way, by asking the migration what it would still change, which resolves
each record's owner through whatever refers to it. Reading the attribute alone would have
understated completeness by a quarter of a million records — and an early draft of this report
did exactly that.</p>
</div>
<div class="scroll">
<table>
<thead><tr><th>Shared payments after the migration</th><th class="n">Count</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><strong>Owned by more than one order</strong></td><td class="n good">0</td><td>whether the orders belong to different clients or the same one</td></tr>
<tr><td class="dim">checked across</td><td class="n dim">400,000 orders</td><td class="dim">spread through the whole database</td></tr>
</tbody>
</table>
</div>
<div class="measure">
<p>The problem this work exists to solve is gone: no payment answers to two orders, so the
component relationship means what it says and deleting an order can no longer take another
order's money with it.</p>
<p><strong>One behaviour changed when the pass was run over everything, and it is worth
recording.</strong> Where Square splits one tender across two of a single client's own orders,
the earlier design left the payment shared on purpose — both orders compute the same name, so
there is no second name for a copy to take. That rule only holds for two orders processed in
the same batch. Run across nineteen million orders in batches of two thousand, such pairs
almost always fall in different batches and the second order now takes a copy. Inside the
ninety-day window this changed nothing measurable: the recompute after the database-wide pass
matched the one before it to the cent. Outside the window it has not been measured, and it
should be before this runs against production.</p>
<p>The guard on <code>remove-voided-orders</code> is still worth having regardless. It is
cheap, and it makes the safety a property of the deletion rather than of the migration having
been run first.</p>
</div>
<div class="callout">
<span class="h">Re-running is safe, and that was proved at full scale</span>
<p>After the complete pass, asking the migration what it would change next returns
<strong>nothing</strong> — 17,047,142 payments examined, none to rename, none unscopable. A
record that already carries the right name is left untouched, so the migration can be stopped,
resumed, or repeated without consequence.</p>
<p>Its speed is worth a note for whoever schedules it: the whole nineteen million orders were
walked in about <strong>thirteen minutes</strong>. An earlier attempt appeared to be
transactor-bound and was projected at two days, which is why a previous run narrowed it to the
analysis window. That diagnosis was wrong. The bottleneck was garbage collection in the process
driving the migration — freeing held memory took an unrelated recompute from 17 client-days a
minute to 4,515. Nothing about the database or the transactor needed to change.</p>
</div>
<div class="measure">
<p><strong>What follows from the gate reading zero.</strong> <code>unscoped-report</code> counts
these figures on demand. Now that unscoped is zero across the board, the importer's
understanding of the old name form can be removed — at which point two clients sharing a
location becomes structurally incapable of producing a shared record, rather than prevented by
a convention that a future import could quietly break. That removal is the one remaining step
of this piece of work.</p>
</div>
</section>
<section>
<h2>Decisions and risks still open</h2>
<div class="scroll">
<table>
<thead><tr><th>Item</th><th>Who decides</th><th>Why it matters</th></tr></thead>
<tbody>
<tr><td>Which client record survives at each shared location</td><td>the business</td><td>the newer record generally has no history before the split, so keeping it loses years of the location's books</td></tr>
<tr><td>Which revenue account service charges post to</td><td>accounting</td><td>currently 49000 Service Income, chosen so the work could be measured; it affects reporting, never whether a day balances</td></tr>
<tr><td>Recognising a return on a refund-only day</td><td>accounting</td><td>posts to 41300 Returns, the account already used for returns; it moves the recognition date, so a refund settling after month end lands in the later period</td></tr>
<tr><td>Refunds on days that <em>did</em> trade</td><td>accounting</td><td>deliberately untouched — apportioning a return across a day that also sold is a judgement call, and 108 days remain out of balance because of it</td></tr>
<tr><td>Whether to correct records the wrong client already owns</td><td>the business</td><td>the fix stops future mix-ups; it does not retrospectively move records claimed while the configuration was shared</td></tr>
<tr><td><code>remove-voided-orders</code></td><td>engineering</td><td>safe once no payment has two parent orders; worth guarding regardless so it detaches rather than deletes</td></tr>
</tbody>
</table>
</div>
<div class="callout warn">
<span class="h">Two operational findings, unrelated to the summaries</span>
<p><strong>The production backup had not written a restore point since 2025-03-10</strong> — roughly seventeen months — even though data files were still uploading daily. A backup you cannot restore from is not a backup. A fresh one was taken on 2026-08-14 and is what this work used.</p>
<p><strong>The database server was sized for a toy dataset</strong>: a 2 GB cache against 27 GB of data. Worth checking what production is set to.</p>
<p><strong>Slowness here was misdiagnosed twice, in the same direction.</strong> Both a recompute crawling at 17 client-days a minute and a migration projected to take two days turned out to be garbage collection in the client process, not the database or the transactor. Freeing held memory took the recompute to 4,515 client-days a minute — a factor of 265 — and the migration finished in thirteen minutes. The lesson generalises: before concluding the transactor is the bottleneck, look at the heap of whatever is driving it.</p>
</div>
</section>
<section>
<h2>How to check any of this <span class="tech">technical</span></h2>
<div class="scroll">
<pre><span class="dim">;; the restored database, untouched production as of 2026-08-14 22:52</span>
(def conn (d/connect "datomic:dev://localhost:4337/integreat-prod-restore"))
<span class="dim">;; the two orders behind the worked examples</span>
(d/pull (d/db conn) '[*] [:sales-order/external-id
"square/order/NGLK-SM-OxSX9gpXJV394qqT8mnBGypUwKNZY"])
(d/pull (d/db conn) '[*] [:sales-order/external-id
"square/order/NTPT-PT-KrMZzcon1cpQEJUyetErkBIpcdEZY"])
<span class="dim">;; the gate: no payment may have two parent orders</span>
(rk/charges-with-multiple-parents (d/db conn) orders) <span class="dim">;; =&gt; 0</span>
<span class="dim">;; ownership history — which records ever changed client</span>
(->> (d/datoms (d/history (d/db conn)) :aevt :sales-refund/client)
(filter :added)
(reduce (fn [m d] (update m (:e d) (fnil conj #{}) (:v d))) {})
(filter (fn [[_ owners]] (&gt; (count owners) 1)))
count)</pre>
</div>
<div class="measure">
<p>The comparison tool is committed as <code>auto-ap.jobs.compare-sales-summaries</code>. Unit tests: <code>lein test auto-ap.jobs.sales-summaries-test auto-ap.square.core3-test auto-ap.jobs.rekey-square-external-ids-test</code>.</p>
</div>
<div class="callout warn">
<span class="h">Do not compare summaries with <code>as-of</code> — a correction to how this was measured</span>
<p>The obvious way to audit a recompute is to read the database at a point before it and diff:
Datomic keeps every past value, so no snapshot is needed. That is what
<code>compare-sales-summaries</code> was built to do, and for summary amounts it does not work.
<code>:ledger-mapped/amount</code>, <code>:ledger-mapped/ledger-side</code> and
<code>:ledger-mapped/account</code> are all declared <code>:db/noHistory true</code>, so
superseded values are discarded rather than retained. A historical read of a summary that has
since been recomputed can return its lines with the categories intact and the amounts simply
absent — which reads as a legitimate all-zero summary, not as an error.</p>
<p>Every figure in this report is therefore taken from a live read of the database immediately
after each pass, captured and stored outside it, and the before/after comparison is done
between those two captures. No historical read is involved anywhere in the numbers above. The
tool remains useful for categories and for which days changed; its docstring overstates what it
can recover, and that is worth correcting before someone relies on it for amounts.</p>
</div>
</section>
<footer>
<span>Measured 2026-08-15 against <span class="mono">integreat-prod-restore</span>, restored fresh from backup point 209608347 — production as of 2026-08-14 22:52. Nothing in production was read or written. Branch <span class="mono">worktree-sales-summary-balance</span>.</span>
<span>A day counts as out of balance when money taken minus money earned is half a penny or more. "Material" means ten cents or more, the threshold below which the residual is till rounding. Of the 123 remaining days, 33 are material.</span>
<span>Both the baseline and the result are live captures taken straight after their own recompute, never historical reads — see the note on <code>as-of</code> above.</span>
</footer>
</div>

View File

@@ -0,0 +1,217 @@
# Sales-summary balancing — rollout plan
Steps to execute, in order. Every step is either reversible or verifiable before the next one
begins. Both behaviour changes are behind per-client feature flags that are **off by default**, so
merging and deploying this branch changes nothing on its own.
Measured on a restored copy of production (backup point `209608347`), 210 clients over
2026-05-10 → 2026-08-07: **1,258 client-days out of balance / $69,560.10 → 123 days / $2,970.35**,
with zero days knocked out of balance and zero already-balanced days altered.
---
## Before you start
| | |
|---|---|
| Branch | `worktree-sales-summary-balance` |
| Flags introduced | `summary-service-charges`, `summary-refund-only-returns` — both off by default |
| Migration to run once | `auto-ap.jobs.rekey-square-external-ids/migrate-all!` |
| Expected migration runtime | ~13 minutes for 19M orders on a warm cache |
| Nothing here touches | invoices, payments, the ledger, or any client without the flags set |
**One prerequisite that is not code.** Ten Square locations are configured against two client records
each. Someone in the business has to decide which record survives at each. The newer record usually
has no history from before the split, so keeping it loses years of that location's books. Do this
before step 3.
---
## Step 1 — Deploy the code
Deploy the branch as normal. Both flags are absent from every client, so:
- tips are calculated exactly as they are today,
- no `Service Charges` line is written,
- no `Returns` line is written on refund-only days.
The only changes that take effect immediately are the safe ones: imbalance logging, the
dirty-summary scan bounded to one client (1,321 ms → 5.6 ms per client), the schema-ordering fix,
and the importer's new client-scoped keys.
**The importer starts writing client-scoped keys straight away, and reads both schemes.** That is
deliberate and is what makes the deploy independent of the migration. Do not remove the legacy
lookup in `square.core3/existing-id` yet — see step 8.
**Verify before moving on.** After one nightly import cycle:
```clojure
;; refunds, payouts and shifts must not have doubled
(count (d/datoms (d/db conn) :aevt :sales-refund/external-id))
(count (d/datoms (d/db conn) :aevt :expected-deposit/external-id))
(count (d/datoms (d/db conn) :aevt :cash-drawer-shift/external-id))
```
Compare against the same counts taken immediately before deploy. Growth should be ordinary daily
volume. A near-doubling means the legacy fallback is not working — **stop and roll back the deploy**.
---
## Step 2 — Guard `remove-voided-orders`
Do this before the migration, not after. `:sales-order/charges` is `:db/isComponent true`, so
retracting an order cascades into its payments. Until step 3 finishes there are still payments with
two parent orders, and deleting one client's voided order can take the other client's payment with
it.
Either leave `remove-voided-orders` switched off until step 3 completes, or change it to detach a
payment that has more than one parent rather than delete it. Detaching is worth doing regardless —
it makes the safety a property of the deletion rather than of the migration having been run first.
See `docs/2026-08-15-remove-voided-orders-risk.md`.
---
## Step 3 — Retire the duplicate client records
Business decision from the top of this document. Deactivate the losing record's Square location so
the importer stops fetching for it. The record itself stays; its history is untouched.
**Verify:** no Square location is configured against two active client records.
---
## Step 4 — Run the migration
```clojure
(require '[auto-ap.jobs.rekey-square-external-ids :as rk])
;; read-only first — check :collisions is empty for every attribute
(dissoc (rk/plan (d/db conn) :charge/external-id rk/charge-prefix) :new-keys)
;; then the whole thing
(rk/migrate-all! 2000)
```
Runs in about thirteen minutes over 19M orders. It is **idempotent and resumable** — a record that
already carries the right name is skipped, so it can be stopped and re-run without consequence.
If it appears to crawl, the cause is almost certainly garbage collection in the process driving it,
not the transactor. That misdiagnosis cost two days of projected runtime during this work. Free
retained memory in the REPL and re-measure before changing anything about the database.
**Verify — all four must read zero to migrate and zero unscopable:**
```clojure
(rk/unscoped-report (d/db conn))
(dissoc (rk/plan (d/db conn) :charge/external-id rk/charge-prefix) :new-keys)
;; => {:total 17047142 :to-migrate 0 :already-scoped 17047142 :unscopable 0}
;; and the gate that this work exists for
(rk/charges-with-multiple-parents (d/db conn) (take 400000 (rk/all-order-ids (d/db conn))))
;; => 0
```
Note `unscoped-report`'s `:no-owner` column is not a gap: ~283k payout-stub payments carry no
`:charge/client` attribute of their own, so it cannot verify them by attribute. `plan` resolves
ownership through whatever refers to them and is the figure to trust.
---
## Step 5 — Recompute summaries, flags still off
```clojure
(require '[auto-ap.jobs.sales-summaries :as ss])
(ss/refresh-sales-summaries 90)
```
This is the pass that banks the deduplication. **Capture the result before going further** — you
will need it as the baseline for step 6, and it cannot be reconstructed afterwards:
```clojure
(require '[auto-ap.tools.compare-sales-summaries :as cmp]) ; test/dev classpath
(def before (cmp/summaries-in (d/db conn) start end))
(spit "before.edn" (pr-str before))
```
> **Do not use `d/as-of` to compare summary amounts.** `:ledger-mapped/amount`, `ledger-side` and
> `account` are `:db/noHistory`, so past values are discarded. A summary that has since been
> recomputed reads back through `as-of` with its amounts *absent*, which looks like a legitimate
> balanced day. Capture live, before and after, and diff the captures.
---
## Step 6 — Turn the flags on, a few restaurants at a time
Needs accounting sign-off first, on two points:
- `summary-service-charges` posts to **49000 Service Income**. Chosen so the work could be measured.
It affects reporting, never whether a day balances.
- `summary-refund-only-returns` posts to **41300 Returns**, the account already used for returns. It
moves the *recognition date* of a return onto the day the refund settled, so a refund settling
after month end lands in the later period.
```clojure
@(d/transact conn [{:db/id [:client/code "NGxx"]
:client/feature-flags ["summary-service-charges"
"summary-refund-only-returns"]}])
(ss/refresh-sales-summaries 90)
```
Start with two or three restaurants, confirm, then widen.
**Verify** against the capture from step 5:
```clojure
(def after (cmp/summaries-in (d/db conn) start end))
(cmp/compare-window ...) ; both arguments live database values, never as-of
```
The two numbers that matter — both were zero across all 18,900 client-days in testing:
- `:balanced->unbalanced` must be **0**
- previously-balanced days whose lines changed must be **0**
If either is non-zero, retract the flags for the affected clients and re-run step 5. The flags are
the rollback: removing them restores today's behaviour exactly.
---
## Step 7 — Re-enable `remove-voided-orders`
Safe once step 4's gate reads zero. Keep the detach-rather-than-delete guard from step 2.
---
## Step 8 — Remove the legacy key lookup
Only once `plan` reports `:to-migrate 0` and has stayed there through several import cycles. Drop
the second branch of `square.core3/existing-id`. At that point two clients sharing a location
becomes structurally incapable of producing a shared record, rather than prevented by a convention a
future import could break.
This is the last step and there is no hurry.
---
## What this will not fix
123 client-days over ninety days, $2,970.35, of which only 33 are above ten cents.
| | Days | Variance | |
|---|---:|---:|---|
| Real trading days with genuine discrepancies | 108 | $1,995.36 | the NGBR/NGBK tender gap, the ezCater fee question, unexplained clusters on NGMV and NGEB |
| Processing fee on a day with no trading | 15 | $974.99 | same shape as the refund-only day, but the offsetting entry belongs to the payout, not to sales |
Refunds landing on days that *did* trade are deliberately left alone. Apportioning a return across a
day that also sold is a judgement call, and guessing at it is how correct books get quietly
rewritten.
---
## Two operational findings, unrelated to the summaries
- **The production backup had not written a restore point since 2025-03-10** — about seventeen
months — although data files were still uploading daily. Worth an alert on restore-point age.
- **The database server is sized for a much smaller dataset**: a 2 GB cache against 27 GB of data.
Worth checking what production is set to.

View File

@@ -1 +1 @@
1`

View File

@@ -0,0 +1 @@
,noti,pop-os,01.06.2026 21:02,file:///home/noti/.config/libreoffice/4;

File diff suppressed because one or more lines are too long

View File

@@ -6,10 +6,10 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
4,PAPER & DISP,STRAW PLAS TRANS JMB WRPD 7.75,Paper Costs,55000,
5,PAPER & DISP,FORK PLAS BLK PLA COMPOSTABLE,Paper Costs,55000,
6,PAPER & DISP,BAG PLAS LOGO 3 CLR,Paper Costs,55000,
7,FROZEN,BREAD PITA GYRO PRE-OILED 7,Food Costs,50000,
7,FROZEN,BREAD PITA GYRO PRE-OILED 7,Bread and Bun Costs,51400,
8,DAIRY PRODUCTS,YOGURT FRZN TART,Dairy Costs,51300,
9,POULTRY,GYRO CHICKEN SHAWARMA CONE,Chicken/ Poultry Costs,51120,
10,FROZEN,BAKLAVA CLASSIC 2X24,Food Costs,50000,
10,FROZEN,BAKLAVA CLASSIC 2X24,Dry Goods Costs,51500,
11,MEATS,PORK SLI GYRO CONE,Beef/Pork Costs,51110,
12,DAIRY PRODUCTS,SAUCE TZATZIKI,Dairy Costs,51300,
13,POULTRY,CHICKEN CVP THIGH BNLS SKLS,Chicken/ Poultry Costs,51120,
@@ -19,7 +19,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
17,CANNED AND DRY,RICE BASMATI STEAMED XTRA LNG,Food Costs,50000,
18,CANNED AND DRY,WATER SPARKLING GREEK,Beverages Costs,52000,
19,DAIRY PRODUCTS,SAUCE SPICY YOGURT LOGO,Dairy Costs,51300,
20,CANNED AND DRY,WATER PURIFIED .5,Food Costs,50000,
20,CANNED AND DRY,WATER PURIFIED .5,Soft Beverage Cost,52000,
21,FROZEN,DOUGH PASTRY HNY PUFF,Food Costs,50000,
22,DAIRY PRODUCTS,YOGURT PLAIN GREEK NON-FAT,Dairy Costs,51300,
23,PAPER & DISP,GLOVE NITRILE LARGE,Paper Costs,55000,
@@ -43,39 +43,39 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
41,CANNED AND DRY,WATER BOTTLED DRINKING,Food Costs,50000,
42,CANNED AND DRY,SODA CHERRY VISSINADA GRK PLAS,Food Costs,50000,
43,CANNED AND DRY,SODA LEMON LEMONADA GREEK,Soft Beverage Costs,52000,
44,CANNED AND DRY,WATER MINERAL CARNONATED GREEK,Food Costs,50000,
44,CANNED AND DRY,WATER MINERAL CARNONATED GREEK,Soft Beverage Cost,52000,
45,DISPENSER BEVRG,SYRUP COLA PEPSI BIB,Soft Beverage Costs,52000,
46,DISPENSER BEVRG,SYRUP LEMONADE PNK BIB,Soft Beverage Costs,52000,
47,CANNED AND DRY,RICE BASMATI PABROIL SELA CS,Food Costs,50000,
48,CANNED AND DRY,KETCHUP FANCY,Food Costs,50000,
47,CANNED AND DRY,RICE BASMATI PABROIL SELA CS,Dry Goods Costs,51500,
48,CANNED AND DRY,KETCHUP FANCY,Dry Goods Costs,51500,
49,CANNED AND DRY,TUB & HUMMUS,Food Costs,50000,
50,CHEMICAL/JANTRL,SANITIZER MULTI QUAT LIQ,Food Costs,50000,
50,CHEMICAL/JANTRL,SANITIZER MULTI QUAT LIQ,Cleaning Supplies,74100,
51,DAIRY PRODUCTS,YOGURT PLAIN GRK 5%,Dairy Costs,51300,
52,FROZEN,APTZR VEG FALAFEL BALL,Food Costs,50000,
52,FROZEN,APTZR VEG FALAFEL BALL,Dry Goods Costs,51500,
53,PAPER & DISP,BOWL PAPER FIBER RND 32OZ 8IN,Paper Costs,55000,
54,PAPER & DISP,LID PLAS F/BOWL RND 8,Paper Costs,55000,
55,MEATS,BEEF GRND CHUCK FINE 80/20FRSH,Beef/Pork Costs,51110,
56,CANNED AND DRY,SODA ORANGE CRSH,Food Costs,50000,
56,CANNED AND DRY,SODA ORANGE CRSH,Soft Beverage Cost,52000,
57,PAPER & DISP,CONTAINER PLAS CLR BAR LK 5 IN,Paper Costs,55000,
58,CANNED AND DRY,KETCHUP PACKET FCY,Food Costs,50000,
58,CANNED AND DRY,KETCHUP PACKET FCY,Dry Goods Costs,51500,
59,PAPER & DISP,BAG PLAS WAVE TOP LOGO 18X16,Paper Costs,55000,
60,CANNED AND DRY,DRESSING VINAIGRETTE LOGO,Food Costs,50000,
60,CANNED AND DRY,DRESSING VINAIGRETTE LOGO,Dressing & Sauce Cost,51450,
61,PAPER & DISP,CONTAINER PAPER MLD FBR 9X6,Paper Costs,55000,
62,PAPER & DISP,BOWL PAPER MLD FBR 32OZ NFA,Paper Costs,55000,
63,PAPER & DISP,CONTAINER PLAS 120Z SUNDAE,Paper Costs,55000,
64,CANNED AND DRY,DRESSING VINAIGRETTE GYRO,Food Costs,50000,
65,CANNED AND DRY,VINEGAR WINE RED 5% 50 GRN,Alcohol Costs,54000,
66,DAIRY PRODUCTS,EGG SHELL LG WHT AA CA CGFREE,Dairy Costs,51300,
67,CANNED AND DRY,DRESSING MARINADE SOUVLAKI,Food Costs,50000,
68,CANNED AND DRY,SAUCE MUSTARD,Food Costs,50000,
67,CANNED AND DRY,DRESSING MARINADE SOUVLAKI,Dressing & Sauce Cost,51450,
68,CANNED AND DRY,SAUCE MUSTARD,Dressing & Sauce Cost,51450,
69,CANNED AND DRY,TEA ICED SWEET PURELEAF,Beverages Costs,52000,
70,PAPER & DISP,LINER TRASH 40X46 1.1 ML GRY,Paper Costs,55000,
71,CANNED AND DRY,SODA COLA,Soft Beverage Costs,52000,
72,CANNED AND DRY,HONEY PURE CLOVER GR A TSC JUG,Food Costs,50000,
72,CANNED AND DRY,HONEY PURE CLOVER GR A TSC JUG,Dry Goods Costs,51500,
73,DAIRY PRODUCTS,CHEESE FETA RW,Dairy Costs,51300,
74,CANNED AND DRY,WATER PURIFIED BTL PET LSE DW,Food Costs,50000,
74,CANNED AND DRY,WATER PURIFIED BTL PET LSE DW,Soft Beverage Cost,52000,
75,PRODUCE,JUICE LEMON FRESH PSTRZD,Produce Costs,51200,
76,CANNED AND DRY,SPREAD HUMMUS TRADITIONAL,Food Costs,50000,
76,CANNED AND DRY,SPREAD HUMMUS TRADITIONAL,Dressing & Sauce Cost,51450,
77,PRODUCE,LETTUCE ROMAINE OF HEART FRSH,Produce Costs,51200,
78,PAPER & DISP,CONTAINER PAPER #1/30OZ NTG,Paper Costs,55000,
79,CANNED AND DRY,OIL SALAD CANOLA ZTF,Food Costs,50000,
@@ -84,7 +84,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
82,POULTRY,CHICKEN CVP WHL WOG NAE 3.5-4#,Chicken/ Poultry Costs,51120,
83,PAPER & DISP,GLOVE NITRILE FDSRV PF BLU LRG,Paper Costs,55000,
84,CANNED AND DRY,RICE BASMATI CHEF SECRT LG GRN,Food Costs,50000,
85,FROZEN,APTZR VEG FALAFEL PUCK HALAL,Food Costs,50000,
85,FROZEN,APTZR VEG FALAFEL PUCK HALAL,Dry Goods Costs,51500,
86,PAPER & DISP,LID PLAS PET FOR 32OZ BOWL,Paper Costs,55000,
87,PAPER & DISP,FORK PLAS PP X-HVY BLK,Paper Costs,55000,
88,PRODUCE,TOMATO ROMA JUMBO FRESH,Produce Costs,51200,
@@ -95,7 +95,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
93,PRODUCE,SPINACH BABY FRSH,Produce Costs,51200,
94,PRODUCE,DILL BABY FRESH HERB,Produce Costs,51200,
95,PRODUCE,CUCUMBER ENGLISH MED SEEDLESS,Produce Costs,51200,
96,CANNED AND DRY,SODA ORANGE PORTOKALADA GREEK,Food Costs,50000,
96,CANNED AND DRY,SODA ORANGE PORTOKALADA GREEK,Soft Beverage Cost,52000,
97,DAIRY PRODUCTS,BUTTER SOLID USDA AA UNSLTD,Dairy Costs,51300,
98,DAIRY PRODUCTS,CHEESE MONT JACK SLI INT .75OZ,Dairy Costs,51300,
99,DAIRY PRODUCTS,CREAMER HALF & HALF SHF STBL,Dairy Costs,51300,
@@ -115,18 +115,18 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
113,CANNED AND DRY,FLOUR ALL PURP H&R BL EN MT,Food Costs,50000,
114,CANNED AND DRY,JAM STRAWBERRY CUP,Food Costs,50000,
115,CANNED AND DRY,MARMALADE ORANGE CUP,Food Costs,50000,
116,CANNED AND DRY,SALT GRANULATED PLAIN,Food Costs,50000,
116,CANNED AND DRY,SALT GRANULATED PLAIN,Dry Goods Costs,51500,
117,CANNED AND DRY,SAUCE HOT PEPPER CALIFN STYLE,Food Costs,50000,
118,CANNED AND DRY,SAUCE STEAK GLASS,Food Costs,50000,
119,CANNED AND DRY,SHORTENING PAN & GRILL,Food Costs,50000,
120,CANNED AND DRY,SUGAR GRANULATED XFINE CANE,Food Costs,50000,
120,CANNED AND DRY,SUGAR GRANULATED XFINE CANE,Dry Goods Costs,51500,
121,CANNED AND DRY,SYRUP BREAKFAST CUP,Food Costs,50000,
122,CANNED AND DRY,SYRUP PANCAKE & WAFFLE,Food Costs,50000,
123,PAPER & DISP,BAG PLAS TSHRT 11.5X6.5X21 TKU,Paper Costs,55000,
124,PAPER & DISP,CONTAINER PLAS DELI TRANS W/LD,Paper Costs,55000,
125,PAPER & DISP,CONTAINER PLAS HNG WHT 8.5 1C,Paper Costs,55000,
126,PAPER & DISP,CUP PLAS PRTN TRANS 2OZ,Paper Costs,55000,
127,PAPER & DISP,FOIL ALMN ROLL STD WGT 500FT,Paper Costs,55000,
127,PAPER & DISP,FOIL ALMN ROLL STD WGT 500FT,Dry Goods Costs,51500,
128,PAPER & DISP,LINER TRASH 40X46 1.6 ML BLK,Paper Costs,55000,
129,PAPER & DISP,TOWEL MULTI 9.5X9.12 EARTH+,Paper Costs,55000,
130,PRODUCE,ASPARAGUS FRESH LARGE FX,Produce Costs,51200,
@@ -155,7 +155,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
153,DAIRY PRODUCTS,MILK WHL CORRUGATE,Dairy Costs,51300,
154,DAIRY PRODUCTS,BUTTERMILK 1% HG,Dairy Costs,51300,
155,DAIRY PRODUCTS,CHEESE CHDR MLD SLI INT .75 YL,Dairy Costs,51300,
156,CHEMICAL/JANTRL,BLEACH LIQ GRMCDL ULTRA 6%,Food Costs,50000,
156,CHEMICAL/JANTRL,BLEACH LIQ GRMCDL ULTRA 6%,Cleaning Supplies,74100,
157,POULTRY,TURKEY BRST NAT BRN PAN SKON,Poultry Costs,51120,
158,DAIRY PRODUCTS,CREAM HEAVY 40% FRESH HG,Dairy Costs,51300,
159,MEATS,BACON SHINGLE 10/12 HY GF PR12,Beef/Pork Costs,51110,
@@ -194,7 +194,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
192,PAPER & DISP,BOWL PAPER MOLDED FIBER 32OZ,Paper Costs,55000,
193,PAPER & DISP,BOX CORR CATER #1 LOGO 2021,Paper Costs,55000,
194,DISPENSER BEVRG,SYRUP LEMONADE BIB,Soft Beverage Costs,52000,
195,PAPER & DISP,FOIL ALMN ROLL HVY WGT 500 FT,Paper Costs,55000,
195,PAPER & DISP,FOIL ALMN ROLL HVY WGT 500 FT,Dry Goods Costs,51500,
196,DAIRY PRODUCTS,CHEESE FETA CHUNKS IN BRNE,Dairy Costs,51300,
197,POULTRY,CHICKEN CVP WOG WHL HAL,Chicken/ Poultry Costs,51120,
198,PAPER & DISP,CONTAINER MFPP 1C HNG 9X6 WHT,Paper Costs,55000,
@@ -203,11 +203,11 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
201,DISPENSER BEVRG,SYRUP LEMON LIME BIB,Soft Beverage Costs,52000,
202,DAIRY PRODUCTS,CHEESE FETA PAIL,Dairy Costs,51300,
203,CANNED AND DRY,CHGS FOR MINIMUM ORDER,Food Costs,50000,
204,CANNED AND DRY,BREAD CRUMB PLAIN MED,Food Costs,50000,
205,CANNED AND DRY,DRESSING SALAD PRASINI,Food Costs,50000,
206,CANNED AND DRY,OIL CORN,Food Costs,50000,
207,CANNED AND DRY,OLIVE KALAMATA PTD BRNE 22 LB,Food Costs,50000,
208,CANNED AND DRY,SPICE TURMERIC GROUND,Food Costs,50000,
204,CANNED AND DRY,BREAD CRUMB PLAIN MED,Dry Goods Costs,51500,
205,CANNED AND DRY,DRESSING SALAD PRASINI,Dressing & Sauce Cost,51450,
206,CANNED AND DRY,OIL CORN,Dressing & Sauce Cost,51450,
207,CANNED AND DRY,OLIVE KALAMATA PTD BRNE 22 LB,Produce Costs,51200,
208,CANNED AND DRY,SPICE TURMERIC GROUND,Dry Goods Costs,51500,
209,PRODUCE,SQUASH ZUCCHINI MEDIUM FRESH,Produce Costs,51200,
210,PRODUCE,ONION GREEN ICELS ROOTLESS,Produce Costs,51200,
211,CANNED AND DRY,SODA COLA PEPSI ZERO,Soft Beverage Costs,52000,
@@ -219,7 +219,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
217,FROZEN,BAKLAVA GREEK PASTRY,Food Costs,50000,
218,DISPENSER BEVRG,SYRUP DR PPR DIET BIB,Soft Beverage Costs,52000,
219,DISPENSER BEVRG,SYRUP MOUNTAIN DEW BIB,Soft Beverage Costs,52000,
220,CANNED AND DRY,SAUCE CHILI HOT SRIRACHA,Food Costs,50000,
220,CANNED AND DRY,SAUCE CHILI HOT SRIRACHA,Dry Goods Costs,51500,
221,PAPER & DISP,SKEWER BAMBOO 10IN,Paper Costs,55000,
222,CANNED AND DRY,RICE BASMATI,Food Costs,50000,
223,PAPER & DISP,WRAP PAPER 14X14 LOGO,Paper Costs,55000,
@@ -227,12 +227,12 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
225,MEATS,PORK BUTT BNLS VP PR12,Beef/Pork Costs,51110,
226,DAIRY PRODUCTS,YOGURT PLAIN GREEK NONFAT,Dairy Costs,51300,
227,PAPER & DISP,TONG PLAS 9 BLK SNAP N SERVE,Paper Costs,55000,
228,CANNED AND DRY,SAUCE HOT SRIRACHA,Food Costs,50000,
228,CANNED AND DRY,SAUCE HOT SRIRACHA,Dry Goods Costs,51500,
229,DISPENSER BEVRG,SYRUP DR PEPPER BIB,Soft Beverage Costs,52000,
230,HLTHCAR/HOSPITALITY,BILLING MISC REGULAR,Food Costs,50000,
231,PAPER & DISP,FORK PLAS BLK MEDHVY MDLNGTH,Paper Costs,55000,
232,DAIRY PRODUCTS,YOGURT PLAIN ORIGINAL FTFR,Dairy Costs,51300,
233,SUPP & EQUIP,MOP HEAD BLND LPD ALL PURP LRG,Food Costs,50000,
233,SUPP & EQUIP,MOP HEAD BLND LPD ALL PURP LRG,Cleaning Supplies,74100,
234,PAPER & DISP,SPOON PLAS WHT MEDHVY MDLNGTH,Paper Costs,55000,
235,MEATS,BEEF GROUND BULK NAT 80/20,Beef/Pork Costs,51110,
236,PAPER & DISP,CONTAINER PAPER HNG 9X6 PFF,Paper Costs,55000,
@@ -245,9 +245,9 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
243,PRODUCE,CUCUMBER ENGLISH FRSH,Produce Costs,51200,
244,PAPER & DISP,FORK PLAS WHT MED HVY MDLNGTH,Paper Costs,55000,
245,PAPER & DISP,CUP PLAS 12-14OZ CLR STRT WALL,Paper Costs,55000,
246,CANNED AND DRY,SAUCE HOT BOTTLE,Food Costs,50000,
247,CANNED AND DRY,OIL OLIVE BLEND 80/20,Food Costs,50000,
248,CANNED AND DRY,SPICE OREGANO LEAF RUBBED,Food Costs,50000,
246,CANNED AND DRY,SAUCE HOT BOTTLE,Dry Goods Costs,51500,
247,CANNED AND DRY,OIL OLIVE BLEND 80/20,Dry Goods Costs,51500,
248,CANNED AND DRY,SPICE OREGANO LEAF RUBBED,Dry Goods Costs,51500,
249,DAIRY PRODUCTS,YOGURT VANILLA GREEK NFAT,Dairy Costs,51300,
250,FROZEN,BUN BRIOCHE HOMESTYLE 4,Food Costs,50000,
251,CANNED AND DRY,WATER SPRKLG IMPRTD MNERAL GLS,Food Costs,50000,
@@ -260,18 +260,18 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
258,CANNED AND DRY,PEPPER GREEN CHILI WHL,Food Costs,50000,
259,CANNED AND DRY,PEPPER JALAPENO SLI FIELD RUN,Food Costs,50000,
260,CANNED AND DRY,SAUCE MIX HOLLANDAISE GF,Food Costs,50000,
261,CANNED AND DRY,VINEGAR DISTILLED WHITE 5%,Food Costs,50000,
261,CANNED AND DRY,VINEGAR DISTILLED WHITE 5%,Dry Goods Costs,51500,
262,PAPER & DISP,COVER TOILET SEAT,Paper Costs,55000,
263,PAPER & DISP,FILM PVC 2000FT ROLL,Paper Costs,55000,
264,PAPER & DISP,DOILY PAPER NRMDY LACE 6,Paper Costs,55000,
265,PAPER & DISP,KIT CUTLERY MED PP KFS S&P NAP,Paper Costs,55000,
266,PAPER & DISP,NAPKIN DNR 2P 15X16.25 1/8F WH,Paper Costs,55000,
267,CHEMICAL/JANTRL,DETERGENT POT/PAN LIQ PINK RTU,Food Costs,50000,
267,CHEMICAL/JANTRL,DETERGENT POT/PAN LIQ PINK RTU,Cleaning Supplies,74100,
268,CHEMICAL/JANTRL,SALT GRANULE SOLAR WATER SOFT,Food Costs,50000,
269,PRODUCE,CARROT FRESH JUMBO,Produce Costs,51200,
270,PRODUCE,LIME FRESH 200CT,Produce Costs,51200,
271,PAPER & DISP,CUP PLAS RPET CLR 16 OZ,Paper Costs,55000,
272,CANNED AND DRY,SAUCE HOT,Food Costs,50000,
272,CANNED AND DRY,SAUCE HOT,Dry Goods Costs,51500,
273,MEATS,BACON SHINGLE 10/12 AW GF PR12,Beef/Pork Costs,51110,
274,PAPER & DISP,CRAYON RED BLUE YEL GREEN,Paper Costs,55000,
275,DAIRY PRODUCTS,CREAMER HALF AND HALF PC ASEP,Dairy Costs,51300,
@@ -282,7 +282,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
280,CANNED AND DRY,KETCHUP SQUEEZE UPSIDE DOWN,Food Costs,50000,
281,PAPER & DISP,GLOVE NITRILE FDSRV PF BLK LRG,Paper Costs,55000,
282,MEATS,BEEF GRND CHUCK 81/19 CHUB FRS,Beef/Pork Costs,51110,
283,PAPER & DISP,LID FOIL F/FULL STM TBL PAN,Paper Costs,55000,
283,PAPER & DISP,LID FOIL F/FULL STM TBL PAN,Dry Goods Costs,51500,
284,PAPER & DISP,FORK WOODEN DISP,Paper Costs,55000,
285,PAPER & DISP,SKEWER BAMBOO THIN 8 IN,Paper Costs,55000,
286,PAPER & DISP,WRAP DELI WHT 12X12 GRS RESIST,Paper Costs,55000,
@@ -292,9 +292,9 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
290,PAPER & DISP,TRAY FOOD PAPER #2 LOGO,Paper Costs,55000,
291,PAPER & DISP,LID PLAS CLR F/1.5-2.5OZ PRTN,Paper Costs,55000,
292,DISPENSER BEVRG,SYRUP TEA RASP 5X1 BRISK,Soft Beverage Costs,52000,
293,PAPER & DISP,PAN FOIL STM TBL FULL DP 3-3/8,Paper Costs,55000,
293,PAPER & DISP,PAN FOIL STM TBL FULL DP 3-3/8,Dry Goods Costs,51500,
294,DISPENSER BEVRG,SYRUP ROOT BEER BIB,Soft Beverage Costs,52000,
295,PAPER & DISP,FOIL ALMN ROLL HVY WGT 1000 FT,Paper Costs,55000,
295,PAPER & DISP,FOIL ALMN ROLL HVY WGT 1000 FT,Dry Goods Costs,51500,
296,DAIRY PRODUCTS,CHEESE GORGONZOLA WHEEL HALF,Dairy Costs,51300,
297,DAIRY PRODUCTS,CHEESE MOZZ WM SHRED GOLD PREM,Dairy Costs,51300,
298,DAIRY PRODUCTS,CREAM SOUR CULTRD GRADE A,Dairy Costs,51300,
@@ -311,7 +311,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
309,SEAFOOD,SCALLOP SEA WTR ADD 10/20 USA,Seafood Costs,51130,
310,MEATS,BACON SLAB SLI 13/17 CT PR12,Beef/Pork Costs,51110,
311,CANNED AND DRY,DRESSING MIX RANCH,Food Costs,50000,
312,FROZEN,BUN BRIOCHE HOMESTYLE 4.25,Food Costs,50000,
312,FROZEN,BUN BRIOCHE HOMESTYLE 4.25,Bread and Bun Costs,51400,
313,CANNED AND DRY,SODA LEMON LIME,Soft Beverage Costs,52000,
314,FROZEN,PUREE ORANGE BLOOD CONCENTRATE,Food Costs,50000,
315,CANNED AND DRY,FLOUR HI-GLUTEN BL EN MT AA,Dry Good Costs,51500,
@@ -327,23 +327,23 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
325,DAIRY PRODUCTS,CHEESE FETA CHUNKS PAIL PREM,Dairy Costs,51300,
326,PRODUCE,PEPPER GREEN BELL LARGE FRESH,Produce Costs,51200,
327,PRODUCE,TOMATO ROMA FRSH,Produce Costs,51200,
328,CHEMICAL/JANTRL,CLEANER DEGREASER OVEN RTU,Food Costs,50000,
328,CHEMICAL/JANTRL,CLEANER DEGREASER OVEN RTU,Cleaning Supplies,74100,
329,PAPER & DISP,BOX CORR CATER #4 LOGO 2021,Paper Costs,55000,
330,PAPER & DISP,LID PLAS HI DOME DESSRT,Paper Costs,55000,
331,MEATS,PORK BUTT BNLS 1/4 6-9#EA,Beef/Pork Costs,51110,
332,PAPER & DISP,CUP PAPER HOT WHT TALL 12OZ,Paper Costs,55000,
333,PAPER & DISP,SPOON PLAS SOUP BLACK XHEAVY,Paper Costs,55000,
334,PAPER & DISP,FOIL ALMN ROLL STD WGT 1000 FT,Paper Costs,55000,
335,SUPP & EQUIP,PAD SCRUB STNLS 50GR 1.75OZ,Food Costs,50000,
334,PAPER & DISP,FOIL ALMN ROLL STD WGT 1000 FT,Dry Goods Costs,51500,
335,SUPP & EQUIP,PAD SCRUB STNLS 50GR 1.75OZ,Cleaning Supplies,74100,
336,DISPENSER BEVRG,SYRUP TEA UNSWTD 5X1,Soft Beverage Costs,52000,
337,FROZEN,BUN BRIOCHE SPLIT TOP 4IN SLI,Food Costs,50000,
337,FROZEN,BUN BRIOCHE SPLIT TOP 4IN SLI,Bread and Bun Costs,51400,
338,CANNED AND DRY,WATER MINERAL LIMONATA CAN,Beverages Costs,52000,
339,PRODUCE,GARLIC PEELED CHINESE,Produce Costs,51200,
340,PAPER & DISP,LID PLAS FLAT F/12-24Z PET CUP,Paper Costs,55000,
341,FROZEN,BILLING MISC FROZEN,Food Costs,50000,
342,PAPER & DISP,KNIFE PLAS BLK PLA COMPSTABLE,Paper Costs,55000,
343,PAPER & DISP,GLOVE NITRILE MED,Paper Costs,55000,
344,SUPP & EQUIP,GRILL BRICK 3.5IN THICK,Food Costs,50000,
344,SUPP & EQUIP,GRILL BRICK 3.5IN THICK,Cleaning Supplies,74100,
345,PRODUCE,CABBAGE SAVOY FRSH,Produce Costs,51200,
346,PRODUCE,FLOWER ORCHID MULTI COLORED,Produce Costs,51200,
347,CANNED AND DRY,OIL AVOCADO,Food Costs,50000,
@@ -393,7 +393,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
391,PAPER & DISP,TOWEL MULTIFOLD 9.4X9.2 WHT 1P,Paper Costs,55000,
392,PAPER & DISP,TISSUE TOILET WRPD 4X3.8 2PLY,Paper Costs,55000,
393,MEATS,BEEF GRND 80/20 BULK,Beef/Pork Costs,51110,
394,CANNED AND DRY,MAYONNAISE HEAVY DUTY,Food Costs,50000,
394,CANNED AND DRY,MAYONNAISE HEAVY DUTY,Dressing & Sauce Cost,51450,
395,PAPER & DISP,CONTAINER PLAS HNG 9X6 WHT,Paper Costs,55000,
396,PAPER & DISP,SPOON PLASTIC BLK BAGGED,Paper Costs,55000,
397,PAPER & DISP,FORK PLAS BLK HVY FULL LNGTH,Paper Costs,55000,
@@ -406,7 +406,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
404,CANNED AND DRY,BEAN BLACK,Food Costs,50000,
405,CANNED AND DRY,SYRUP CHOCOLATE PLAS JUG,Food Costs,50000,
406,CANNED AND DRY,TUNA LIGHT SKIPJACK CHUNK WTR,Food Costs,50000,
407,CHEMICAL/JANTRL,DEGREASER HEAVY DUTY RTU,Food Costs,50000,
407,CHEMICAL/JANTRL,DEGREASER HEAVY DUTY RTU,Cleaning Supplies,74100,
408,CANNED AND DRY,JAM BLACKBERRY CUP,Food Costs,50000,
409,CANNED AND DRY,PICKLE WHL DILL KO REF 75/85,Food Costs,50000,
410,DAIRY PRODUCTS,CHEESE QUESO FRESCO CASERO,Dairy Costs,51300,
@@ -423,7 +423,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
421,PAPER & DISP,CONTAINER PAPER HNG 9X6 FIB,Paper Costs,55000,
422,PAPER & DISP,LINER REPRO 40X46 1.5 ML BLK,Paper Costs,55000,
423,PAPER & DISP,TAPE PAPR REG THERMAL 3-1/8,Paper Costs,55000,
424,SUPP & EQUIP,PAD SCOUR GRN 6X9IN ANTIMICRO,Food Costs,50000,
424,SUPP & EQUIP,PAD SCOUR GRN 6X9IN ANTIMICRO,Cleaning Supplies,74100,
425,PAPER & DISP,GLOVE NITRILE BLUE XL,Paper Costs,55000,
426,PRODUCE,CUCUMBER ENGLISH LONG,Produce Costs,51200,
427,PRODUCE,TOMATO ROMA MED,Produce Costs,51200,
@@ -434,13 +434,13 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
432,POULTRY,CHICKEN CVP THIGH B/S HALAL JM,Chicken/ Poultry Costs,51120,
433,CANNED AND DRY,DRESSING HONEY MUSTARD,Food Costs,50000,
434,PRODUCE,JUICE LEMON PSTRZD ULTRA PREM,Produce Costs,51200,
435,CANNED AND DRY,SPICE PAPRIKA GROUND,Food Costs,50000,
435,CANNED AND DRY,SPICE PAPRIKA GROUND,Dry Goods Costs,51500,
436,PAPER & DISP,FORK PLAS WHT HVY FULL LENGTH,Paper Costs,55000,
437,PAPER & DISP,LID FOIL F/ HALF STMTBL PAN,Paper Costs,55000,
437,PAPER & DISP,LID FOIL F/ HALF STMTBL PAN,Dry Goods Costs,51500,
438,SUPP & EQUIP,PAN FOIL HALF DEEP 100CT,Food Costs,50000,
439,PAPER & DISP,CONTAINER FOAM HNG LRG 1C,Paper Costs,55000,
440,PAPER & DISP,LINER TRASH 40X48 13 MC NAT,Paper Costs,55000,
441,CANNED AND DRY,KETCHUP FCY,Food Costs,50000,
441,CANNED AND DRY,KETCHUP FCY,Dry Goods Costs,51500,
442,PAPER & DISP,STRAW PLAS WRPD FLEX WHT 7.625,Paper Costs,55000,
443,CANNED AND DRY,WATER SPRKLG CHRY/POMGRNT,Food Costs,50000,
444,PAPER & DISP,APRON POLY EMBSD WHT 28X46 ECO,Paper Costs,55000,
@@ -457,13 +457,13 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
455,SEAFOOD,CALAMARI TUBE & TNT 5-8 INCH,Seafood Costs,51130,
456,SEAFOOD,SHRIMP CKD & PLD BAY 90\150 FZ,Seafood Costs,51130,
457,SEAFOOD,SHRIMP WHT GH 16-20,Seafood Costs,51130,
458,SUPP & EQUIP,BROOM ANGULAR FLAGGED,Food Costs,50000,
458,SUPP & EQUIP,BROOM ANGULAR FLAGGED,Cleaning Supplies,74100,
459,CANNED AND DRY,SAUCE CHILI SRIRACHA,Food Costs,50000,
460,PAPER & DISP,CONTAINER PAPER #1 TK OUT KRFT,Paper Costs,55000,
461,PAPER & DISP,SPOON PLAS BLK MEDHVY MDLNGTH,Paper Costs,55000,
462,PAPER & DISP,LID PLAS F/ 12/16/21/24 CUPS,Paper Costs,55000,
463,PAPER & DISP,GLOVE NITRILE FDSRV PF BLU XL,Paper Costs,55000,
464,CANNED AND DRY,SODA ORANGE,Food Costs,50000,
464,CANNED AND DRY,SODA ORANGE,Soft Beverage Cost,52000,
465,PRODUCE,SQUASH ZUCCHINI MED FRSH,Produce Costs,51200,
466,CANNED AND DRY,SPICE TURMERIC GRND ORGANIC,Food Costs,50000,
467,CANNED AND DRY,WATER SPRING IMPORTED GLS,Food Costs,50000,
@@ -474,9 +474,9 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
472,PAPER & DISP,GLOVE NITRILE BLK PEDRFREE LRG,Paper Costs,55000,
473,PAPER & DISP,TRAY CARRYOUT 4CUP,Paper Costs,55000,
474,PAPER & DISP,BAG PLAS T-SHRT THNKYOU12X7X22,Paper Costs,55000,
475,PAPER & DISP,GRILL BRICK 3.5IN THICK,Paper Costs,55000,
476,PAPER & DISP,PAD SCOUR GRN 6X9IN ANTIMICRO,Paper Costs,55000,
477,PAPER & DISP,PAD SCRUB STNLS 50GR 1.75OZ,Paper Costs,55000,
475,PAPER & DISP,GRILL BRICK 3.5IN THICK,Cleaning Supplies,74100,
476,PAPER & DISP,PAD SCOUR GRN 6X9IN ANTIMICRO,Cleaning Supplies,74100,
477,PAPER & DISP,PAD SCRUB STNLS 50GR 1.75OZ,Cleaning Supplies,74100,
478,PAPER & DISP,LID TOGO PLAS F/12-16-32 OZ,Paper Costs,55000,
479,PRODUCE,PEPPER GREEN BELL FRSH LG,Produce Costs,51200,
480,DISPENSER BEVRG,SYRUP COLA WILD CHERRY,Soft Beverage Costs,52000,
@@ -486,25 +486,25 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
484,PRODUCE,TOMATO ROMA FRESH,Produce Costs,51200,
485,DAIRY PRODUCTS,YOGURT PLAIN GREEK WM 4% FAT,Dairy Costs,51300,
486,CHEMICAL/JANTRL,CLEANER OVEN GREASESTRIP+ NP,Food Costs,50000,
487,FROZEN,BUN BRIOCHE SLI 4.5,Food Costs,50000,
487,FROZEN,BUN BRIOCHE SLI 4.5,Bread and Bun Costs,51400,
488,PRODUCE,LEMON FRESH,Produce Costs,51200,
489,SUPP & EQUIP,DISH SUPREME GLASS,Food Costs,50000,
490,PAPER & DISP,CONTAINER POLYETHELYN,Paper Costs,55000,
491,PAPER & DISP,FILTER GREASE CONE 10 IN,Paper Costs,55000,
491,PAPER & DISP,FILTER GREASE CONE 10 IN,Cleaning Supplies,74100,
492,PRODUCE,SQUASH ZUCCHINI FCY FRESH,Produce Costs,51200,
493,CHEMICAL/JANTRL,CLEANER ALL PURPOSE PINE RTU,Food Costs,50000,
494,CANNED AND DRY,KETCHUP PACKET FCY FOIL,Food Costs,50000,
493,CHEMICAL/JANTRL,CLEANER ALL PURPOSE PINE RTU,Cleaning Supplies,74100,
494,CANNED AND DRY,KETCHUP PACKET FCY FOIL,Dry Goods Costs,51500,
495,PRODUCE,MUSHROOM PORTABELLA CP LRG FSH,Produce Costs,51200,
496,PAPER & DISP,CONTAINER MINERAL 9X6 HNG 1CPT,Paper Costs,55000,
497,PAPER & DISP,WRAP DRY WAX DELI HVY 10X10.75,Paper Costs,55000,
498,PAPER & DISP,LID PLAS 12/16/22 OZ CUP,Paper Costs,55000,
499,CHEMICAL/JANTRL,POLISH S-S SATIN SHINE ARSL,Food Costs,50000,
500,CANNED AND DRY,SPICE MARJORAM LVS,Food Costs,50000,
501,SUPP & EQUIP,BOTTLE PLASTIC SQUEEZE WIDEMTH,Food Costs,50000,
502,PAPER & DISP,PAN FOIL STM TBL DEEPXH 2-9/16,Paper Costs,55000,
499,CHEMICAL/JANTRL,POLISH S-S SATIN SHINE ARSL,Cleaning Supplies,74100,
500,CANNED AND DRY,SPICE MARJORAM LVS,Dry Goods Costs,51500,
501,SUPP & EQUIP,BOTTLE PLASTIC SQUEEZE WIDEMTH,Paperware Cost,55000,
502,PAPER & DISP,PAN FOIL STM TBL DEEPXH 2-9/16,Dry Goods Costs,51500,
503,PAPER & DISP,TONG PLAS BLK 6.25IN SM SRVING,Paper Costs,55000,
504,PAPER & DISP,FORK PLAS WHT P/P,Paper Costs,55000,
505,CHEMICAL/JANTRL,CLEANER DEGRSR HGH TMP GRL RTU,Food Costs,50000,
505,CHEMICAL/JANTRL,CLEANER DEGRSR HGH TMP GRL RTU,Cleaning Supplies,74100,
506,DISPENSER BEVRG,SYRUP BASE ORG CRSH BIB,Soft Beverage Costs,52000,
507,PAPER & DISP,CONTAINER PAPER #4 TAKEOUT WHT,Paper Costs,55000,
508,CANNED AND DRY,CAPER NONPAREIL IMPORTED,Food Costs,50000,
@@ -515,7 +515,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
513,PAPER & DISP,FORK PLASTIC WRPD PP XHVY BLK,Paper Costs,55000,
514,MEATS,LAMB RIBLET FZN,Meats Costs,51110,
515,CANNED AND DRY,WATER BOTTLED,Food Costs,50000,
516,CHEMICAL/JANTRL,SOAP HAND LIQ PINK RTU,Food Costs,50000,
516,CHEMICAL/JANTRL,SOAP HAND LIQ PINK RTU,Cleaning Supplies,74100,
517,PAPER & DISP,LINER TRASH 40X46 1.5 ML BLU,Paper Costs,55000,
518,CANNED AND DRY,SYSCO CUSTOMER AGREEMENT,Food Costs,50000,
519,CANNED AND DRY,HONEY WILDFLOWER BLOSSOM,Food Costs,50000,
@@ -532,12 +532,12 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
530,CANNED AND DRY,JUICE APPLE GLASS FCY,Food Costs,50000,
531,DAIRY PRODUCTS,MILK ALMOND BARISTA BLEND,Dairy Costs,51300,
532,CANNED AND DRY,SPICE OREGANO LEAF,Food Costs,50000,
533,CANNED AND DRY,SAUCE CHILI SRIRACHA CHA,Food Costs,50000,
533,CANNED AND DRY,SAUCE CHILI SRIRACHA CHA,Dry Goods Costs,51500,
534,CANNED AND DRY,OLIVE KALAMATA PTD PLAS KEG,Food Costs,50000,
535,CANNED AND DRY,MUSTARD YELLOW PRPD,Food Costs,50000,
536,CHEMICAL/JANTRL,CLEANER DEGRSR GREASELIFT RTU,Food Costs,50000,
537,CANNED AND DRY,SALT PKT .6 GM,Food Costs,50000,
538,CANNED AND DRY,SPICE PEPPER PACKET .1 GM,Food Costs,50000,
538,CANNED AND DRY,SPICE PEPPER PACKET .1 GM,Dry Goods Costs,51500,
539,PAPER & DISP,LINER ROLL COMPOST47X60 1ML,Paper Costs,55000,
540,CANNED AND DRY,WATER SPRKLG ORG ARANCAT CAN,Food Costs,50000,
541,DISPENSER BEVRG,SYRUP COKE DIET 5X1 BIB,Soft Beverage Costs,52000,
@@ -564,7 +564,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
562,CHEMICAL/JANTRL,BLEACH LIQUID DISINFECT CLENER,Food Costs,50000,
563,PAPER & DISP,CONTAINER PAPER FBR 9X6 1CPFF,Paper Costs,55000,
564,CANNED AND DRY,SODA COKE MEXICO GLASS NON RET,Soft Beverage Costs,52000,
565,CANNED AND DRY,SPICE CINNAMON STICK,Food Costs,50000,
565,CANNED AND DRY,SPICE CINNAMON STICK,Dry Goods Costs,51500,
566,CANNED AND DRY,WALNUT HALVES AND PCS,Food Costs,50000,
567,DISPENSER BEVRG,TEA ICED CONC RASP 5.5+1,Soft Beverage Costs,52000,
568,MEATS,BEEF GRND BULK 81/19 CHUB FRS,Beef/Pork Costs,51110,
@@ -658,7 +658,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
656,PAPER & DISP,CUP PLAS TRANS 16OZ SOFT,Paper Costs,55000,
657,PRODUCE,PARSLEY BUNCH FDSVC,Produce Costs,51200,
658,PAPER & DISP,LINER TRASH 24X32 .5 ML BLK,Paper Costs,55000,
659,CANNED AND DRY,SPICE CINNAMON GRND,Food Costs,50000,
659,CANNED AND DRY,SPICE CINNAMON GRND,Dry Goods Costs,51500,
660,PAPER & DISP,FORK PLAS HVY STY BLK,Paper Costs,55000,
661,PAPER & DISP,SPOON PLAS PP HVY BLK FULL LEN,Paper Costs,55000,
662,DAIRY PRODUCTS,EGG SHELL LG PAST CF,Dairy Costs,51300,
@@ -797,7 +797,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
795,PAPER & DISP,SUPPLY ACCESSORIES SOTF COM,Paper Costs,55000,
796,PAPER & DISP,LID PLAS CLR FLT W/SLT 12-24OZ,Paper Costs,55000,
797,PAPER & DISP,TRAY PAPER PULP CARRYOUT 4 CUP,Paper Costs,55000,
798,CHEMICAL/JANTRL,SANITIZER OASIS 146 MULTI QUAT,Food Costs,50000,
798,CHEMICAL/JANTRL,SANITIZER OASIS 146 MULTI QUAT,Cleaning Supplies,74100,
799,PAPER & DISP,CUP PLAS CLR TALL 8OZ RIGID,Paper Costs,55000,
800,PAPER & DISP,BILLING MISC DISP,Paper Costs,55000,
801,PAPER & DISP,LID FOIL F/ HALF STM TBL PAN,Paper Costs,55000,
@@ -816,7 +816,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
814,CANNED AND DRY,FLOUR SEMOLINA UNBLCH,Food Costs,50000,
815,CANNED AND DRY,SAUCE HOT SRIRACHA BLUE AGAVE,Food Costs,50000,
816,PAPER & DISP,LINER TRASH 43X48 16 MC NAT,Paper Costs,55000,
817,CANNED AND DRY,WALNUT HALF & PCS,Food Costs,50000,
817,CANNED AND DRY,WALNUT HALF & PCS,Produce Costs,51200,
818,MEATS,BEEF SHORT RIB ASIAN CUT 1/4,Beef/Pork Costs,51110,
819,PAPER & DISP,LINER PLAS INSERT/WARMER 18X14,Paper Costs,55000,
820,PAPER & DISP,BOX PIZZA 14 W/K B-FLT 1-7/8,Paper Costs,55000,
@@ -877,7 +877,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
875,DAIRY PRODUCTS,CHEESE CREAM LIGHT CUP,Dairy Costs,51300,
876,DAIRY PRODUCTS,YOGURT BLUEBERRY GREEK NON FAT,Dairy Costs,51300,
877,HLTHCAR/HOSPITALITY,PERKS MEMBERSHIP FEE,Food Costs,50000,
878,CANNED AND DRY,DRESSING RED WINE VINGRT METRO,Wine Costs,54400,
878,CANNED AND DRY,DRESSING RED WINE VINGRT METRO,Dressing & Sauce Cost,51450,
879,CANNED AND DRY,SPICE GARLIC PWDR,Food Costs,50000,
880,CANNED AND DRY,SPICE ONION POWDER,Food Costs,50000,
881,CANNED AND DRY,SALT KOSHER FLAKE COARSE,Food Costs,50000,
@@ -925,7 +925,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
923,CANNED AND DRY,SAUCE WORCESTERSHIRE,Food Costs,50000,
924,PAPER & DISP,CONTAINER PLAS 1C HNG 6X6 WHT,Paper Costs,55000,
925,PRODUCE,PINEAPPLE FRESH,Produce Costs,51200,
926,SUPP & EQUIP,MOP HEAD CTN CUT END VALUE #24,Food Costs,50000,
926,SUPP & EQUIP,MOP HEAD CTN CUT END VALUE #24,Cleaning Supplies,74100,
927,DAIRY PRODUCTS,CHEESE RICOTTA WMHM SEL,Dairy Costs,51300,
928,MEATS,BACON LAYFLAT NT CC 13/17 PR12,Beef/Pork Costs,51110,
929,CANNED AND DRY,COOKIE CRUMB OREO MED CRUNCH,Food Costs,50000,
@@ -1005,7 +1005,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1004,DAIRY PRODUCTS,CHEESE BLUE STUFFED OLIVES,Dairy Costs,51300,
1005,SUPP & EQUIP,HANDLE MOP FIBRGLS QUICK CHNGE,Food Costs,50000,
1006,SUPP & EQUIP,SUPPLY SOTF JANSAN,Food Costs,50000,
1007,CANNED AND DRY,WALNUT HALVES & PCS,Food Costs,50000,
1007,CANNED AND DRY,WALNUT HALVES & PCS,Produce Costs,51200,
1008,CANNED AND DRY,WATER SPARKLN ORG PRCKLY PEAR,Beverages Costs,52000,
1009,CANNED AND DRY,DRINK NATURAL CLMTN SPRKLG,Food Costs,50000,
1010,CANNED AND DRY,DRESSING MIX RNCH BTRMK NO MSG,Food Costs,50000,
@@ -1032,7 +1032,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1031,CANNED AND DRY,DRESSING BALSAMIC VINGT GARLIC,Food Costs,50000,
1032,CANNED AND DRY,SPREAD CHOC NUTELLA JAR FDSRV,Food Costs,50000,
1033,CANNED AND DRY,SUGAR BROWN LIGHT,Food Costs,50000,
1034,PAPER & DISP,PAD SCOUR 6X9 HVYDTY ANTIMICRO,Paper Costs,55000,
1034,PAPER & DISP,PAD SCOUR 6X9 HVYDTY ANTIMICRO,Cleaning Supplies,74100,
1035,CHEMICAL/JANTRL,DETERGENT POT/PAN LIQ GRN RTU,Food Costs,50000,
1036,PAPER & DISP,KNIFE PLAS HVY STY BLK,Paper Costs,55000,
1037,CHEMICAL/JANTRL,CLEANER DISINFECT PEROX RTU,Food Costs,50000,
@@ -1111,7 +1111,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1110,PAPER & DISP,BAG PAPER BRN W/HNDL REGAL 65#,Paper Costs,55000,
1111,DAIRY PRODUCTS,EGG SHELL WHT CAGEFREE GR A LG,Dairy Costs,51300,
1112,CANNED AND DRY,VINEGAR RICE SEASONED,Food Costs,50000,
1113,CANNED AND DRY,VINEGAR WHITE DSTD 5%,Food Costs,50000,
1113,CANNED AND DRY,VINEGAR WHITE DSTD 5%,Dry Goods Costs,51500,
1114,SEAFOOD,SHRIMP WHT GH 13-15,Seafood Costs,51130,
1115,PAPER & DISP,BAG PLAS PRTN 6.5X7 ORG SAT,Paper Costs,55000,
1116,CANNED AND DRY,BREAD CRUMB JAP PANKO TOASTED,Food Costs,50000,
@@ -1489,7 +1489,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1488,POULTRY,CHICKEN CVP WHL WOG FZ,Chicken/ Poultry Costs,51120,
1489,PRODUCE,LEEK BUNCH FRSH ICELS,Produce Costs,51200,
1490,CANNED AND DRY,BILLING MISC CANNED/DRY,Food Costs,50000,
1491,PAPER & DISP,PAN FOIL STEAM TBL HALF DEEP,Paper Costs,55000,
1491,PAPER & DISP,PAN FOIL STEAM TBL HALF DEEP,Dry Goods Costs,51500,
1492,PAPER & DISP,TRAY PAPER CARRIER 4 CUP,Paper Costs,55000,
1493,PAPER & DISP,KIT CUTLERY FKS/SP/NP HW PP BK,Paper Costs,55000,
1494,MEATS,BEEF PATTY 80/20 RND FRSH,Beef/Pork Costs,51110,
@@ -1498,7 +1498,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1497,DAIRY PRODUCTS,EGG HARDCOOKED CGFREE HARD PK,Dairy Costs,51300,
1498,SEAFOOD,SHRIMP WHT P&D TLOF 26/30,Seafood Costs,51130,
1499,PAPER & DISP,CONTAINER PAPER HNG 9X6 1C FBR,Paper Costs,55000,
1500,CHEMICAL/JANTRL,DETERGENT POT & PAN LIQUID,Food Costs,50000,
1500,CHEMICAL/JANTRL,DETERGENT POT & PAN LIQUID,Cleaning Supplies,74100,
1501,FROZEN,ASPARAGUS SPEAR MED IQF P,Produce Costs,51200,
1502,PRODUCE,ASPARAGUS FRESH STANDARD,Produce Costs,51200,
1503,SUPP & EQUIP,SCREEN GRIDDLE 4X6IN,Food Costs,50000,
@@ -1515,7 +1515,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1514,PAPER & DISP,GLOVE SYNTHETIC FDSRV PF MED,Paper Costs,55000,
1515,POULTRY,CHICKEN THIGH BNLS SKIN-ON RAW,Chicken/ Poultry Costs,51120,
1516,CANNED AND DRY,SPICE SAGE GRND,Food Costs,50000,
1517,CANNED AND DRY,KETCHUP SQUEEZE RED UPSIDE DWN,Food Costs,50000,
1517,CANNED AND DRY,KETCHUP SQUEEZE RED UPSIDE DWN,Dry Goods Costs,51500,
1518,CANNED AND DRY,SPICE NUTMEG WHL,Food Costs,50000,
1519,DAIRY PRODUCTS,YOGURT PLAIN FULL FAT,Dairy Costs,51300,
1520,CANNED AND DRY,VINEGAR WINE RED ITALY 6% GLS,Wine Costs,54400,
@@ -1555,7 +1555,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1554,FROZEN,BAKLAVA WALNT TRIANGLES,Food Costs,50000,
1555,PAPER & DISP,DISPENSER NAP XPRSNP STND BLK,Paper Costs,55000,
1556,PAPER & DISP,DISPENSER TOWEL MANUL COMP360,Paper Costs,55000,
1557,PAPER & DISP,PAN FOIL STM TBL MED 2-3/16,Paper Costs,55000,
1557,PAPER & DISP,PAN FOIL STM TBL MED 2-3/16,Dry Goods Costs,51500,
1558,PRODUCE,ONION RED JUMBO CTN,Produce Costs,51200,
1559,MEATS,BEEF CHUCK SHORTRIB KOREAN1/2,Beef/Pork Costs,51110,
1560,FROZEN,RICE MEXICAN STY,Food Costs,50000,
@@ -1653,7 +1653,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1652,CANNED AND DRY,SODA COKE CHERRY ZERO CONTOUR,Soft Beverage Costs,52000,
1653,FROZEN,ENTREE VEG FALAFEL BALLS VEGAN,Food Costs,50000,
1654,SUPP & EQUIP,BRUSH GRILL W/SCRPR 27 IN HNDL,Food Costs,50000,
1655,CANNED AND DRY,SAUCE HOT SRIRACHA HUY FONG,Food Costs,50000,
1655,CANNED AND DRY,SAUCE HOT SRIRACHA HUY FONG,Dry Goods Costs,51500,
1656,PAPER & DISP,TOWEL MULTIFOLD PRM LEAF,Paper Costs,55000,
1657,CANNED AND DRY,SODA LEMON LIME 12OZ,Soft Beverage Costs,52000,
1658,PAPER & DISP,KNIFE PLAS WRP BLK,Paper Costs,55000,
@@ -1709,7 +1709,7 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1708,PAPER & DISP,CONTAINER PLAS CLR HNG 8IN,Paper Costs,55000,
1709,PAPER & DISP,TISSUE TOILET 2PL ADVC WHT WR,Paper Costs,55000,
1710,SUPP & EQUIP,SPATULA RUBBER SILICONE 10.25,Food Costs,50000,
1711,CANNED AND DRY,BEAN GARBANZO FCY NO SULFITE,Food Costs,50000,
1711,CANNED AND DRY,BEAN GARBANZO FCY NO SULFITE,Dry Goods Costs,51500,
1712,MEATS,BACON SLI APLWD 13/17CT PR12,Beef/Pork Costs,51110,
1713,POULTRY,SAUSAGE CHICKEN APPLE RAW 1 OZ,Poultry Costs,51120,
1714,CANNED AND DRY,TOMATO SUNDRIED JULENNE,Food Costs,50000,
@@ -1759,4 +1759,73 @@ Id,Sysco Category,Sysco Description,Integreat Account,Integreat Account Code,Nic
1758,MEATS,PORK BELLY SKIN ON P12 COV,Beef/Pork Costs,51110,
1759,MEATS,PORK SHANK BONE KUROBUTA PR12,Beef/Pork Costs,51110,
1760,CANNED AND DRY,SEASONING ITALIAN WHL,Food Costs,50000,
1761,PRODUCE,MUSHROOM PORTABELLA CAP 4-5,Produce Costs,51200,
1761,PRODUCE,MUSHROOM PORTABELLA CAP 4-5,Produce Costs,51200,
1762,PAPER & DISP,BAG PAPER 250 CT,Paper Costs,55000,
1763,MEATS,BEEF SHLDR TERES MAJOR SEL,Beef/Pork Costs,51110,
1764,PAPER & DISP,BOWL PLASTIC COATING 42 OZ,Paper Costs,55000,
1765,PAPER & DISP,BOX CATERING 21X13X4.25 LOGO,Paper Costs,55000,
1766,CANNED AND DRY,CANDY MILK CHOC SHELLS,Dry Goods Costs,51500,
1767,CANNED AND DRY,CHOCOLATE DUBAI PISTCHO KUNFEH,Dry Goods Costs,51500,
1768,PAPER & DISP,CONTAINER PAPER 1/30 OZ NTG,Paper Costs,55000,
1769,PAPER & DISP,CONTAINER PAPER 4/110OZ NTG,Paper Costs,55000,
1770,PAPER & DISP,CUP PAPER COLD 22 OZ LOGO NTG,Paper Costs,55000,
1771,PAPER & DISP,CUP PORTION PLAS CLR 1.50 OZ,Paper Costs,55000,
1772,PAPER & DISP,DESSERT CUP,Paper Costs,55000,
1773,FROZEN,DESSERT MINI PLAIN BEIGNET,Bread and Bun Costs,51400,
1774,CANNED AND DRY,DIP GARLIC TOUM,Dressing & Sauce Cost,51450,
1775,CANNED AND DRY,DRINK ENERGY ORANGE SPRKLNG,Soft Beverage Costs,52000,
1776,CANNED AND DRY,DRINK ENERGY PEACH VIBE SPRKLG,Soft Beverage Costs,52000,
1777,CANNED AND DRY,DRINK ENERGY TROPICAL VIBE,Soft Beverage Costs,52000,
1778,PAPER & DISP,FILM PVC 18X2000 ROLL,Paper Costs,55000,
1779,CANNED AND DRY,JUICE CONC MANDARIN CARDAMOM,Soft Beverage Cost,52000,
1780,CANNED AND DRY,JUICE CONC STRAWB DRAGON,Soft Beverage Cost,52000,
1781,PAPER & DISP,LID CLEAR PET 42 OZ,Paper Costs,55000,
1782,PAPER & DISP,LID DOME DESSERT CUP,Paper Costs,55000,
1783,PAPER & DISP,NAPKIN 2PLY INTR FOLD 6.3X8.26,Paper Costs,55000,
1784,CANNED AND DRY,PASTE HERB HARISSA MOROCCAN,Dressing & Sauce Cost,51450,
1785,CANNED AND DRY,PASTE TAHINI DRESSING,Dressing & Sauce Cost,51450,
1786,FROZEN,PASTRY BEIGNET MN FLD CHOCCRML,Bread and Bun Costs,51400,
1787,CANNED AND DRY,PEPPER BANANA MILD RING,Produce Costs,51200,
1788,CANNED AND DRY,RICE MIX NICKS,Dry Goods Costs,51500,
1789,CANNED AND DRY,SODA CHERRY VISSINADA GREEK,Soft Beverage Costs,52000,
1790,CANNED AND DRY,SODA COLA PEPSI ZERO SUGAR,Soft Beverage Costs,52000,
1791,CANNED AND DRY,SODA PEPSI COLA,Soft Beverage Costs,52000,
1792,FROZEN,SPANAKOPITA SPINACH COOKED,Bread and Bun Costs,51400,
1793,PAPER & DISP,SPOON PLAS TEA PP X-HVY BLK,Paper Costs,55000,
1794,PAPER & DISP,WRAP PAPER 14X14 LOGO VER2,Paper Costs,55000,
1795,DAIRY PRODUCTS,YOGURT FRZN NF NICK THE GREEK,Dairy Costs,51300,
1796,FROZEN,BALL FALAFEL FRTTR 1 OZ IQF,Dry Goods Costs,51500,
1797,SUPP & EQUIP,BASKET PLAS 10.5X7X1.5 BLK,Paperware Cost,55000,
1798,CANNED AND DRY,BEAN GARBANZO LOW SODIUM,Dry Goods Costs,51500,
1799,FROZEN,BREAD POTATO ROLL 4 INCH,Bread and Bun Costs,51400,
1800,FROZEN,BUN HAMBURGER 4IN 1.75 OZ,Bread and Bun Costs,51400,
1801,DAIRY PRODUCTS,CHEESE FETA CRUMBLES,Dairy Costs,51300,
1802,FROZEN,CHEESE STICK HALLOUMI STYL,Dairy Costs,51300,
1803,POULTRY,CHICKEN CVP THGH B/S HALAL,Chicken/ Poultry Costs,51120,
1804,CHEMICAL/JANTRL,CLEANER DEGREASER CONCENTR RTU,Cleaning Supplies,74100,
1805,CHEMICAL/JANTRL,CLEANER DEGREASER GRSELFT RTU,Cleaning Supplies,74100,
1806,,CONTAINER PAPER CUSTOM LOGO8X5,Paperware Cost,55000,
1807,,CONTAINER PAPER FBR 9X6 1C WHT,Paperware Cost,55000,
1808,,DRESSING RANCH SPICY,Dairy Costs,51300,
1809,,FILM PVC ROLL CRYS 2000 FT,Paperware Cost,55000,
1810,,FORK PLASTIC SERVING BLK 10IN,Paperware Cost,55000,
1811,DAIRY PRODUCTS,ICE CREAM COOKIE&CREAM,Dairy Costs,51300,
1812,DAIRY PRODUCTS,ICE CREAM STRAWBERRY,Dairy Costs,51300,
1813,DAIRY PRODUCTS,ICE CREAM VAN QUICK BLEND,Dairy Costs,51300,
1814,DISPENSER BEVRG,JUICE CONC BERRY PATCH ORG,Soft Beverage Cost,52000,
1815,CANNED AND DRY,JUICE LEMON PLAS RTU,Produce Costs,51200,
1816,PRODUCE,KALE CHOPPED,Produce Costs,51200,
1817,PRODUCE,KALE FRESH,Produce Costs,51200,
1818,CANNED AND DRY,KETCHUP FCY POUCH EQUALS 6/10#,Dry Goods Costs,51500,
1819,PRODUCE,LEMON FRESH BAGGED,Produce Costs,51200,
1820,CANNED AND DRY,OIL OLIVE SOYBEAN BLEND 75/25,Dry Goods Costs,51500,
1821,PRODUCE,ONION WHITE JUMBO BAG,Produce Costs,51200,
1822,CANNED AND DRY,PEPPER BANANA MILD RING 7-9HUN,Produce Costs,51200,
1823,CANNED AND DRY,PEPPER BANANA RING,Produce Costs,51200,
1824,FROZEN,POTATO FRY 1/4 SS XLF PHANTM,Produce Costs,51200,
1825,,SANITIZER NO RINSE QUORUM,Cleaning Supplies,74100,
1826,CANNED AND DRY,SODA ROOT BEER CUBE,Soft Beverage Cost,52000,
1827,DISPENSER BEVRG,SYRUP FRUIT PUNCH BIB FLASHIN,Soft Beverage Cost,52000,
1828,DISPENSER BEVRG,SYRUP ORANGE 5X1 BIB,Soft Beverage Cost,52000,
1829,PRODUCE,TOMATO BULK 5X5 FRSH,Produce Costs,51200,
1830,PRODUCE,TOMATO GRAPE FRSH,Produce Costs,51200,
1 Id Sysco Category Sysco Description Integreat Account Integreat Account Code Nick's changes
6 4 PAPER & DISP STRAW PLAS TRANS JMB WRPD 7.75 Paper Costs 55000
7 5 PAPER & DISP FORK PLAS BLK PLA COMPOSTABLE Paper Costs 55000
8 6 PAPER & DISP BAG PLAS LOGO 3 CLR Paper Costs 55000
9 7 FROZEN BREAD PITA GYRO PRE-OILED 7 Food Costs Bread and Bun Costs 50000 51400
10 8 DAIRY PRODUCTS YOGURT FRZN TART Dairy Costs 51300
11 9 POULTRY GYRO CHICKEN SHAWARMA CONE Chicken/ Poultry Costs 51120
12 10 FROZEN BAKLAVA CLASSIC 2X24 Food Costs Dry Goods Costs 50000 51500
13 11 MEATS PORK SLI GYRO CONE Beef/Pork Costs 51110
14 12 DAIRY PRODUCTS SAUCE TZATZIKI Dairy Costs 51300
15 13 POULTRY CHICKEN CVP THIGH BNLS SKLS Chicken/ Poultry Costs 51120
19 17 CANNED AND DRY RICE BASMATI STEAMED XTRA LNG Food Costs 50000
20 18 CANNED AND DRY WATER SPARKLING GREEK Beverages Costs 52000
21 19 DAIRY PRODUCTS SAUCE SPICY YOGURT LOGO Dairy Costs 51300
22 20 CANNED AND DRY WATER PURIFIED .5 Food Costs Soft Beverage Cost 50000 52000
23 21 FROZEN DOUGH PASTRY HNY PUFF Food Costs 50000
24 22 DAIRY PRODUCTS YOGURT PLAIN GREEK NON-FAT Dairy Costs 51300
25 23 PAPER & DISP GLOVE NITRILE LARGE Paper Costs 55000
43 41 CANNED AND DRY WATER BOTTLED DRINKING Food Costs 50000
44 42 CANNED AND DRY SODA CHERRY VISSINADA GRK PLAS Food Costs 50000
45 43 CANNED AND DRY SODA LEMON LEMONADA GREEK Soft Beverage Costs 52000
46 44 CANNED AND DRY WATER MINERAL CARNONATED GREEK Food Costs Soft Beverage Cost 50000 52000
47 45 DISPENSER BEVRG SYRUP COLA PEPSI BIB Soft Beverage Costs 52000
48 46 DISPENSER BEVRG SYRUP LEMONADE PNK BIB Soft Beverage Costs 52000
49 47 CANNED AND DRY RICE BASMATI PABROIL SELA CS Food Costs Dry Goods Costs 50000 51500
50 48 CANNED AND DRY KETCHUP FANCY Food Costs Dry Goods Costs 50000 51500
51 49 CANNED AND DRY TUB & HUMMUS Food Costs 50000
52 50 CHEMICAL/JANTRL SANITIZER MULTI QUAT LIQ Food Costs Cleaning Supplies 50000 74100
53 51 DAIRY PRODUCTS YOGURT PLAIN GRK 5% Dairy Costs 51300
54 52 FROZEN APTZR VEG FALAFEL BALL Food Costs Dry Goods Costs 50000 51500
55 53 PAPER & DISP BOWL PAPER FIBER RND 32OZ 8IN Paper Costs 55000
56 54 PAPER & DISP LID PLAS F/BOWL RND 8 Paper Costs 55000
57 55 MEATS BEEF GRND CHUCK FINE 80/20FRSH Beef/Pork Costs 51110
58 56 CANNED AND DRY SODA ORANGE CRSH Food Costs Soft Beverage Cost 50000 52000
59 57 PAPER & DISP CONTAINER PLAS CLR BAR LK 5 IN Paper Costs 55000
60 58 CANNED AND DRY KETCHUP PACKET FCY Food Costs Dry Goods Costs 50000 51500
61 59 PAPER & DISP BAG PLAS WAVE TOP LOGO 18X16 Paper Costs 55000
62 60 CANNED AND DRY DRESSING VINAIGRETTE LOGO Food Costs Dressing & Sauce Cost 50000 51450
63 61 PAPER & DISP CONTAINER PAPER MLD FBR 9X6 Paper Costs 55000
64 62 PAPER & DISP BOWL PAPER MLD FBR 32OZ NFA Paper Costs 55000
65 63 PAPER & DISP CONTAINER PLAS 120Z SUNDAE Paper Costs 55000
66 64 CANNED AND DRY DRESSING VINAIGRETTE GYRO Food Costs 50000
67 65 CANNED AND DRY VINEGAR WINE RED 5% 50 GRN Alcohol Costs 54000
68 66 DAIRY PRODUCTS EGG SHELL LG WHT AA CA CGFREE Dairy Costs 51300
69 67 CANNED AND DRY DRESSING MARINADE SOUVLAKI Food Costs Dressing & Sauce Cost 50000 51450
70 68 CANNED AND DRY SAUCE MUSTARD Food Costs Dressing & Sauce Cost 50000 51450
71 69 CANNED AND DRY TEA ICED SWEET PURELEAF Beverages Costs 52000
72 70 PAPER & DISP LINER TRASH 40X46 1.1 ML GRY Paper Costs 55000
73 71 CANNED AND DRY SODA COLA Soft Beverage Costs 52000
74 72 CANNED AND DRY HONEY PURE CLOVER GR A TSC JUG Food Costs Dry Goods Costs 50000 51500
75 73 DAIRY PRODUCTS CHEESE FETA RW Dairy Costs 51300
76 74 CANNED AND DRY WATER PURIFIED BTL PET LSE DW Food Costs Soft Beverage Cost 50000 52000
77 75 PRODUCE JUICE LEMON FRESH PSTRZD Produce Costs 51200
78 76 CANNED AND DRY SPREAD HUMMUS TRADITIONAL Food Costs Dressing & Sauce Cost 50000 51450
79 77 PRODUCE LETTUCE ROMAINE OF HEART FRSH Produce Costs 51200
80 78 PAPER & DISP CONTAINER PAPER #1/30OZ NTG Paper Costs 55000
81 79 CANNED AND DRY OIL SALAD CANOLA ZTF Food Costs 50000
84 82 POULTRY CHICKEN CVP WHL WOG NAE 3.5-4# Chicken/ Poultry Costs 51120
85 83 PAPER & DISP GLOVE NITRILE FDSRV PF BLU LRG Paper Costs 55000
86 84 CANNED AND DRY RICE BASMATI CHEF SECRT LG GRN Food Costs 50000
87 85 FROZEN APTZR VEG FALAFEL PUCK HALAL Food Costs Dry Goods Costs 50000 51500
88 86 PAPER & DISP LID PLAS PET FOR 32OZ BOWL Paper Costs 55000
89 87 PAPER & DISP FORK PLAS PP X-HVY BLK Paper Costs 55000
90 88 PRODUCE TOMATO ROMA JUMBO FRESH Produce Costs 51200
95 93 PRODUCE SPINACH BABY FRSH Produce Costs 51200
96 94 PRODUCE DILL BABY FRESH HERB Produce Costs 51200
97 95 PRODUCE CUCUMBER ENGLISH MED SEEDLESS Produce Costs 51200
98 96 CANNED AND DRY SODA ORANGE PORTOKALADA GREEK Food Costs Soft Beverage Cost 50000 52000
99 97 DAIRY PRODUCTS BUTTER SOLID USDA AA UNSLTD Dairy Costs 51300
100 98 DAIRY PRODUCTS CHEESE MONT JACK SLI INT .75OZ Dairy Costs 51300
101 99 DAIRY PRODUCTS CREAMER HALF & HALF SHF STBL Dairy Costs 51300
115 113 CANNED AND DRY FLOUR ALL PURP H&R BL EN MT Food Costs 50000
116 114 CANNED AND DRY JAM STRAWBERRY CUP Food Costs 50000
117 115 CANNED AND DRY MARMALADE ORANGE CUP Food Costs 50000
118 116 CANNED AND DRY SALT GRANULATED PLAIN Food Costs Dry Goods Costs 50000 51500
119 117 CANNED AND DRY SAUCE HOT PEPPER CALIFN STYLE Food Costs 50000
120 118 CANNED AND DRY SAUCE STEAK GLASS Food Costs 50000
121 119 CANNED AND DRY SHORTENING PAN & GRILL Food Costs 50000
122 120 CANNED AND DRY SUGAR GRANULATED XFINE CANE Food Costs Dry Goods Costs 50000 51500
123 121 CANNED AND DRY SYRUP BREAKFAST CUP Food Costs 50000
124 122 CANNED AND DRY SYRUP PANCAKE & WAFFLE Food Costs 50000
125 123 PAPER & DISP BAG PLAS TSHRT 11.5X6.5X21 TKU Paper Costs 55000
126 124 PAPER & DISP CONTAINER PLAS DELI TRANS W/LD Paper Costs 55000
127 125 PAPER & DISP CONTAINER PLAS HNG WHT 8.5 1C Paper Costs 55000
128 126 PAPER & DISP CUP PLAS PRTN TRANS 2OZ Paper Costs 55000
129 127 PAPER & DISP FOIL ALMN ROLL STD WGT 500FT Paper Costs Dry Goods Costs 55000 51500
130 128 PAPER & DISP LINER TRASH 40X46 1.6 ML BLK Paper Costs 55000
131 129 PAPER & DISP TOWEL MULTI 9.5X9.12 EARTH+ Paper Costs 55000
132 130 PRODUCE ASPARAGUS FRESH LARGE FX Produce Costs 51200
155 153 DAIRY PRODUCTS MILK WHL CORRUGATE Dairy Costs 51300
156 154 DAIRY PRODUCTS BUTTERMILK 1% HG Dairy Costs 51300
157 155 DAIRY PRODUCTS CHEESE CHDR MLD SLI INT .75 YL Dairy Costs 51300
158 156 CHEMICAL/JANTRL BLEACH LIQ GRMCDL ULTRA 6% Food Costs Cleaning Supplies 50000 74100
159 157 POULTRY TURKEY BRST NAT BRN PAN SKON Poultry Costs 51120
160 158 DAIRY PRODUCTS CREAM HEAVY 40% FRESH HG Dairy Costs 51300
161 159 MEATS BACON SHINGLE 10/12 HY GF PR12 Beef/Pork Costs 51110
194 192 PAPER & DISP BOWL PAPER MOLDED FIBER 32OZ Paper Costs 55000
195 193 PAPER & DISP BOX CORR CATER #1 LOGO 2021 Paper Costs 55000
196 194 DISPENSER BEVRG SYRUP LEMONADE BIB Soft Beverage Costs 52000
197 195 PAPER & DISP FOIL ALMN ROLL HVY WGT 500 FT Paper Costs Dry Goods Costs 55000 51500
198 196 DAIRY PRODUCTS CHEESE FETA CHUNKS IN BRNE Dairy Costs 51300
199 197 POULTRY CHICKEN CVP WOG WHL HAL Chicken/ Poultry Costs 51120
200 198 PAPER & DISP CONTAINER MFPP 1C HNG 9X6 WHT Paper Costs 55000
203 201 DISPENSER BEVRG SYRUP LEMON LIME BIB Soft Beverage Costs 52000
204 202 DAIRY PRODUCTS CHEESE FETA PAIL Dairy Costs 51300
205 203 CANNED AND DRY CHGS FOR MINIMUM ORDER Food Costs 50000
206 204 CANNED AND DRY BREAD CRUMB PLAIN MED Food Costs Dry Goods Costs 50000 51500
207 205 CANNED AND DRY DRESSING SALAD PRASINI Food Costs Dressing & Sauce Cost 50000 51450
208 206 CANNED AND DRY OIL CORN Food Costs Dressing & Sauce Cost 50000 51450
209 207 CANNED AND DRY OLIVE KALAMATA PTD BRNE 22 LB Food Costs Produce Costs 50000 51200
210 208 CANNED AND DRY SPICE TURMERIC GROUND Food Costs Dry Goods Costs 50000 51500
211 209 PRODUCE SQUASH ZUCCHINI MEDIUM FRESH Produce Costs 51200
212 210 PRODUCE ONION GREEN ICELS ROOTLESS Produce Costs 51200
213 211 CANNED AND DRY SODA COLA PEPSI ZERO Soft Beverage Costs 52000
219 217 FROZEN BAKLAVA GREEK PASTRY Food Costs 50000
220 218 DISPENSER BEVRG SYRUP DR PPR DIET BIB Soft Beverage Costs 52000
221 219 DISPENSER BEVRG SYRUP MOUNTAIN DEW BIB Soft Beverage Costs 52000
222 220 CANNED AND DRY SAUCE CHILI HOT SRIRACHA Food Costs Dry Goods Costs 50000 51500
223 221 PAPER & DISP SKEWER BAMBOO 10IN Paper Costs 55000
224 222 CANNED AND DRY RICE BASMATI Food Costs 50000
225 223 PAPER & DISP WRAP PAPER 14X14 LOGO Paper Costs 55000
227 225 MEATS PORK BUTT BNLS VP PR12 Beef/Pork Costs 51110
228 226 DAIRY PRODUCTS YOGURT PLAIN GREEK NONFAT Dairy Costs 51300
229 227 PAPER & DISP TONG PLAS 9 BLK SNAP N SERVE Paper Costs 55000
230 228 CANNED AND DRY SAUCE HOT SRIRACHA Food Costs Dry Goods Costs 50000 51500
231 229 DISPENSER BEVRG SYRUP DR PEPPER BIB Soft Beverage Costs 52000
232 230 HLTHCAR/HOSPITALITY BILLING MISC REGULAR Food Costs 50000
233 231 PAPER & DISP FORK PLAS BLK MEDHVY MDLNGTH Paper Costs 55000
234 232 DAIRY PRODUCTS YOGURT PLAIN ORIGINAL FTFR Dairy Costs 51300
235 233 SUPP & EQUIP MOP HEAD BLND LPD ALL PURP LRG Food Costs Cleaning Supplies 50000 74100
236 234 PAPER & DISP SPOON PLAS WHT MEDHVY MDLNGTH Paper Costs 55000
237 235 MEATS BEEF GROUND BULK NAT 80/20 Beef/Pork Costs 51110
238 236 PAPER & DISP CONTAINER PAPER HNG 9X6 PFF Paper Costs 55000
245 243 PRODUCE CUCUMBER ENGLISH FRSH Produce Costs 51200
246 244 PAPER & DISP FORK PLAS WHT MED HVY MDLNGTH Paper Costs 55000
247 245 PAPER & DISP CUP PLAS 12-14OZ CLR STRT WALL Paper Costs 55000
248 246 CANNED AND DRY SAUCE HOT BOTTLE Food Costs Dry Goods Costs 50000 51500
249 247 CANNED AND DRY OIL OLIVE BLEND 80/20 Food Costs Dry Goods Costs 50000 51500
250 248 CANNED AND DRY SPICE OREGANO LEAF RUBBED Food Costs Dry Goods Costs 50000 51500
251 249 DAIRY PRODUCTS YOGURT VANILLA GREEK NFAT Dairy Costs 51300
252 250 FROZEN BUN BRIOCHE HOMESTYLE 4 Food Costs 50000
253 251 CANNED AND DRY WATER SPRKLG IMPRTD MNERAL GLS Food Costs 50000
260 258 CANNED AND DRY PEPPER GREEN CHILI WHL Food Costs 50000
261 259 CANNED AND DRY PEPPER JALAPENO SLI FIELD RUN Food Costs 50000
262 260 CANNED AND DRY SAUCE MIX HOLLANDAISE GF Food Costs 50000
263 261 CANNED AND DRY VINEGAR DISTILLED WHITE 5% Food Costs Dry Goods Costs 50000 51500
264 262 PAPER & DISP COVER TOILET SEAT Paper Costs 55000
265 263 PAPER & DISP FILM PVC 2000FT ROLL Paper Costs 55000
266 264 PAPER & DISP DOILY PAPER NRMDY LACE 6 Paper Costs 55000
267 265 PAPER & DISP KIT CUTLERY MED PP KFS S&P NAP Paper Costs 55000
268 266 PAPER & DISP NAPKIN DNR 2P 15X16.25 1/8F WH Paper Costs 55000
269 267 CHEMICAL/JANTRL DETERGENT POT/PAN LIQ PINK RTU Food Costs Cleaning Supplies 50000 74100
270 268 CHEMICAL/JANTRL SALT GRANULE SOLAR WATER SOFT Food Costs 50000
271 269 PRODUCE CARROT FRESH JUMBO Produce Costs 51200
272 270 PRODUCE LIME FRESH 200CT Produce Costs 51200
273 271 PAPER & DISP CUP PLAS RPET CLR 16 OZ Paper Costs 55000
274 272 CANNED AND DRY SAUCE HOT Food Costs Dry Goods Costs 50000 51500
275 273 MEATS BACON SHINGLE 10/12 AW GF PR12 Beef/Pork Costs 51110
276 274 PAPER & DISP CRAYON RED BLUE YEL GREEN Paper Costs 55000
277 275 DAIRY PRODUCTS CREAMER HALF AND HALF PC ASEP Dairy Costs 51300
282 280 CANNED AND DRY KETCHUP SQUEEZE UPSIDE DOWN Food Costs 50000
283 281 PAPER & DISP GLOVE NITRILE FDSRV PF BLK LRG Paper Costs 55000
284 282 MEATS BEEF GRND CHUCK 81/19 CHUB FRS Beef/Pork Costs 51110
285 283 PAPER & DISP LID FOIL F/FULL STM TBL PAN Paper Costs Dry Goods Costs 55000 51500
286 284 PAPER & DISP FORK WOODEN DISP Paper Costs 55000
287 285 PAPER & DISP SKEWER BAMBOO THIN 8 IN Paper Costs 55000
288 286 PAPER & DISP WRAP DELI WHT 12X12 GRS RESIST Paper Costs 55000
292 290 PAPER & DISP TRAY FOOD PAPER #2 LOGO Paper Costs 55000
293 291 PAPER & DISP LID PLAS CLR F/1.5-2.5OZ PRTN Paper Costs 55000
294 292 DISPENSER BEVRG SYRUP TEA RASP 5X1 BRISK Soft Beverage Costs 52000
295 293 PAPER & DISP PAN FOIL STM TBL FULL DP 3-3/8 Paper Costs Dry Goods Costs 55000 51500
296 294 DISPENSER BEVRG SYRUP ROOT BEER BIB Soft Beverage Costs 52000
297 295 PAPER & DISP FOIL ALMN ROLL HVY WGT 1000 FT Paper Costs Dry Goods Costs 55000 51500
298 296 DAIRY PRODUCTS CHEESE GORGONZOLA WHEEL HALF Dairy Costs 51300
299 297 DAIRY PRODUCTS CHEESE MOZZ WM SHRED GOLD PREM Dairy Costs 51300
300 298 DAIRY PRODUCTS CREAM SOUR CULTRD GRADE A Dairy Costs 51300
311 309 SEAFOOD SCALLOP SEA WTR ADD 10/20 USA Seafood Costs 51130
312 310 MEATS BACON SLAB SLI 13/17 CT PR12 Beef/Pork Costs 51110
313 311 CANNED AND DRY DRESSING MIX RANCH Food Costs 50000
314 312 FROZEN BUN BRIOCHE HOMESTYLE 4.25 Food Costs Bread and Bun Costs 50000 51400
315 313 CANNED AND DRY SODA LEMON LIME Soft Beverage Costs 52000
316 314 FROZEN PUREE ORANGE BLOOD CONCENTRATE Food Costs 50000
317 315 CANNED AND DRY FLOUR HI-GLUTEN BL EN MT AA Dry Good Costs 51500
327 325 DAIRY PRODUCTS CHEESE FETA CHUNKS PAIL PREM Dairy Costs 51300
328 326 PRODUCE PEPPER GREEN BELL LARGE FRESH Produce Costs 51200
329 327 PRODUCE TOMATO ROMA FRSH Produce Costs 51200
330 328 CHEMICAL/JANTRL CLEANER DEGREASER OVEN RTU Food Costs Cleaning Supplies 50000 74100
331 329 PAPER & DISP BOX CORR CATER #4 LOGO 2021 Paper Costs 55000
332 330 PAPER & DISP LID PLAS HI DOME DESSRT Paper Costs 55000
333 331 MEATS PORK BUTT BNLS 1/4 6-9#EA Beef/Pork Costs 51110
334 332 PAPER & DISP CUP PAPER HOT WHT TALL 12OZ Paper Costs 55000
335 333 PAPER & DISP SPOON PLAS SOUP BLACK XHEAVY Paper Costs 55000
336 334 PAPER & DISP FOIL ALMN ROLL STD WGT 1000 FT Paper Costs Dry Goods Costs 55000 51500
337 335 SUPP & EQUIP PAD SCRUB STNLS 50GR 1.75OZ Food Costs Cleaning Supplies 50000 74100
338 336 DISPENSER BEVRG SYRUP TEA UNSWTD 5X1 Soft Beverage Costs 52000
339 337 FROZEN BUN BRIOCHE SPLIT TOP 4IN SLI Food Costs Bread and Bun Costs 50000 51400
340 338 CANNED AND DRY WATER MINERAL LIMONATA CAN Beverages Costs 52000
341 339 PRODUCE GARLIC PEELED CHINESE Produce Costs 51200
342 340 PAPER & DISP LID PLAS FLAT F/12-24Z PET CUP Paper Costs 55000
343 341 FROZEN BILLING MISC FROZEN Food Costs 50000
344 342 PAPER & DISP KNIFE PLAS BLK PLA COMPSTABLE Paper Costs 55000
345 343 PAPER & DISP GLOVE NITRILE MED Paper Costs 55000
346 344 SUPP & EQUIP GRILL BRICK 3.5IN THICK Food Costs Cleaning Supplies 50000 74100
347 345 PRODUCE CABBAGE SAVOY FRSH Produce Costs 51200
348 346 PRODUCE FLOWER ORCHID MULTI COLORED Produce Costs 51200
349 347 CANNED AND DRY OIL AVOCADO Food Costs 50000
393 391 PAPER & DISP TOWEL MULTIFOLD 9.4X9.2 WHT 1P Paper Costs 55000
394 392 PAPER & DISP TISSUE TOILET WRPD 4X3.8 2PLY Paper Costs 55000
395 393 MEATS BEEF GRND 80/20 BULK Beef/Pork Costs 51110
396 394 CANNED AND DRY MAYONNAISE HEAVY DUTY Food Costs Dressing & Sauce Cost 50000 51450
397 395 PAPER & DISP CONTAINER PLAS HNG 9X6 WHT Paper Costs 55000
398 396 PAPER & DISP SPOON PLASTIC BLK BAGGED Paper Costs 55000
399 397 PAPER & DISP FORK PLAS BLK HVY FULL LNGTH Paper Costs 55000
406 404 CANNED AND DRY BEAN BLACK Food Costs 50000
407 405 CANNED AND DRY SYRUP CHOCOLATE PLAS JUG Food Costs 50000
408 406 CANNED AND DRY TUNA LIGHT SKIPJACK CHUNK WTR Food Costs 50000
409 407 CHEMICAL/JANTRL DEGREASER HEAVY DUTY RTU Food Costs Cleaning Supplies 50000 74100
410 408 CANNED AND DRY JAM BLACKBERRY CUP Food Costs 50000
411 409 CANNED AND DRY PICKLE WHL DILL KO REF 75/85 Food Costs 50000
412 410 DAIRY PRODUCTS CHEESE QUESO FRESCO CASERO Dairy Costs 51300
423 421 PAPER & DISP CONTAINER PAPER HNG 9X6 FIB Paper Costs 55000
424 422 PAPER & DISP LINER REPRO 40X46 1.5 ML BLK Paper Costs 55000
425 423 PAPER & DISP TAPE PAPR REG THERMAL 3-1/8 Paper Costs 55000
426 424 SUPP & EQUIP PAD SCOUR GRN 6X9IN ANTIMICRO Food Costs Cleaning Supplies 50000 74100
427 425 PAPER & DISP GLOVE NITRILE BLUE XL Paper Costs 55000
428 426 PRODUCE CUCUMBER ENGLISH LONG Produce Costs 51200
429 427 PRODUCE TOMATO ROMA MED Produce Costs 51200
434 432 POULTRY CHICKEN CVP THIGH B/S HALAL JM Chicken/ Poultry Costs 51120
435 433 CANNED AND DRY DRESSING HONEY MUSTARD Food Costs 50000
436 434 PRODUCE JUICE LEMON PSTRZD ULTRA PREM Produce Costs 51200
437 435 CANNED AND DRY SPICE PAPRIKA GROUND Food Costs Dry Goods Costs 50000 51500
438 436 PAPER & DISP FORK PLAS WHT HVY FULL LENGTH Paper Costs 55000
439 437 PAPER & DISP LID FOIL F/ HALF STMTBL PAN Paper Costs Dry Goods Costs 55000 51500
440 438 SUPP & EQUIP PAN FOIL HALF DEEP 100CT Food Costs 50000
441 439 PAPER & DISP CONTAINER FOAM HNG LRG 1C Paper Costs 55000
442 440 PAPER & DISP LINER TRASH 40X48 13 MC NAT Paper Costs 55000
443 441 CANNED AND DRY KETCHUP FCY Food Costs Dry Goods Costs 50000 51500
444 442 PAPER & DISP STRAW PLAS WRPD FLEX WHT 7.625 Paper Costs 55000
445 443 CANNED AND DRY WATER SPRKLG CHRY/POMGRNT Food Costs 50000
446 444 PAPER & DISP APRON POLY EMBSD WHT 28X46 ECO Paper Costs 55000
457 455 SEAFOOD CALAMARI TUBE & TNT 5-8 INCH Seafood Costs 51130
458 456 SEAFOOD SHRIMP CKD & PLD BAY 90\150 FZ Seafood Costs 51130
459 457 SEAFOOD SHRIMP WHT GH 16-20 Seafood Costs 51130
460 458 SUPP & EQUIP BROOM ANGULAR FLAGGED Food Costs Cleaning Supplies 50000 74100
461 459 CANNED AND DRY SAUCE CHILI SRIRACHA Food Costs 50000
462 460 PAPER & DISP CONTAINER PAPER #1 TK OUT KRFT Paper Costs 55000
463 461 PAPER & DISP SPOON PLAS BLK MEDHVY MDLNGTH Paper Costs 55000
464 462 PAPER & DISP LID PLAS F/ 12/16/21/24 CUPS Paper Costs 55000
465 463 PAPER & DISP GLOVE NITRILE FDSRV PF BLU XL Paper Costs 55000
466 464 CANNED AND DRY SODA ORANGE Food Costs Soft Beverage Cost 50000 52000
467 465 PRODUCE SQUASH ZUCCHINI MED FRSH Produce Costs 51200
468 466 CANNED AND DRY SPICE TURMERIC GRND ORGANIC Food Costs 50000
469 467 CANNED AND DRY WATER SPRING IMPORTED GLS Food Costs 50000
474 472 PAPER & DISP GLOVE NITRILE BLK PEDRFREE LRG Paper Costs 55000
475 473 PAPER & DISP TRAY CARRYOUT 4CUP Paper Costs 55000
476 474 PAPER & DISP BAG PLAS T-SHRT THNKYOU12X7X22 Paper Costs 55000
477 475 PAPER & DISP GRILL BRICK 3.5IN THICK Paper Costs Cleaning Supplies 55000 74100
478 476 PAPER & DISP PAD SCOUR GRN 6X9IN ANTIMICRO Paper Costs Cleaning Supplies 55000 74100
479 477 PAPER & DISP PAD SCRUB STNLS 50GR 1.75OZ Paper Costs Cleaning Supplies 55000 74100
480 478 PAPER & DISP LID TOGO PLAS F/12-16-32 OZ Paper Costs 55000
481 479 PRODUCE PEPPER GREEN BELL FRSH LG Produce Costs 51200
482 480 DISPENSER BEVRG SYRUP COLA WILD CHERRY Soft Beverage Costs 52000
486 484 PRODUCE TOMATO ROMA FRESH Produce Costs 51200
487 485 DAIRY PRODUCTS YOGURT PLAIN GREEK WM 4% FAT Dairy Costs 51300
488 486 CHEMICAL/JANTRL CLEANER OVEN GREASESTRIP+ NP Food Costs 50000
489 487 FROZEN BUN BRIOCHE SLI 4.5 Food Costs Bread and Bun Costs 50000 51400
490 488 PRODUCE LEMON FRESH Produce Costs 51200
491 489 SUPP & EQUIP DISH SUPREME GLASS Food Costs 50000
492 490 PAPER & DISP CONTAINER POLYETHELYN Paper Costs 55000
493 491 PAPER & DISP FILTER GREASE CONE 10 IN Paper Costs Cleaning Supplies 55000 74100
494 492 PRODUCE SQUASH ZUCCHINI FCY FRESH Produce Costs 51200
495 493 CHEMICAL/JANTRL CLEANER ALL PURPOSE PINE RTU Food Costs Cleaning Supplies 50000 74100
496 494 CANNED AND DRY KETCHUP PACKET FCY FOIL Food Costs Dry Goods Costs 50000 51500
497 495 PRODUCE MUSHROOM PORTABELLA CP LRG FSH Produce Costs 51200
498 496 PAPER & DISP CONTAINER MINERAL 9X6 HNG 1CPT Paper Costs 55000
499 497 PAPER & DISP WRAP DRY WAX DELI HVY 10X10.75 Paper Costs 55000
500 498 PAPER & DISP LID PLAS 12/16/22 OZ CUP Paper Costs 55000
501 499 CHEMICAL/JANTRL POLISH S-S SATIN SHINE ARSL Food Costs Cleaning Supplies 50000 74100
502 500 CANNED AND DRY SPICE MARJORAM LVS Food Costs Dry Goods Costs 50000 51500
503 501 SUPP & EQUIP BOTTLE PLASTIC SQUEEZE WIDEMTH Food Costs Paperware Cost 50000 55000
504 502 PAPER & DISP PAN FOIL STM TBL DEEPXH 2-9/16 Paper Costs Dry Goods Costs 55000 51500
505 503 PAPER & DISP TONG PLAS BLK 6.25IN SM SRVING Paper Costs 55000
506 504 PAPER & DISP FORK PLAS WHT P/P Paper Costs 55000
507 505 CHEMICAL/JANTRL CLEANER DEGRSR HGH TMP GRL RTU Food Costs Cleaning Supplies 50000 74100
508 506 DISPENSER BEVRG SYRUP BASE ORG CRSH BIB Soft Beverage Costs 52000
509 507 PAPER & DISP CONTAINER PAPER #4 TAKEOUT WHT Paper Costs 55000
510 508 CANNED AND DRY CAPER NONPAREIL IMPORTED Food Costs 50000
515 513 PAPER & DISP FORK PLASTIC WRPD PP XHVY BLK Paper Costs 55000
516 514 MEATS LAMB RIBLET FZN Meats Costs 51110
517 515 CANNED AND DRY WATER BOTTLED Food Costs 50000
518 516 CHEMICAL/JANTRL SOAP HAND LIQ PINK RTU Food Costs Cleaning Supplies 50000 74100
519 517 PAPER & DISP LINER TRASH 40X46 1.5 ML BLU Paper Costs 55000
520 518 CANNED AND DRY SYSCO CUSTOMER AGREEMENT Food Costs 50000
521 519 CANNED AND DRY HONEY WILDFLOWER BLOSSOM Food Costs 50000
532 530 CANNED AND DRY JUICE APPLE GLASS FCY Food Costs 50000
533 531 DAIRY PRODUCTS MILK ALMOND BARISTA BLEND Dairy Costs 51300
534 532 CANNED AND DRY SPICE OREGANO LEAF Food Costs 50000
535 533 CANNED AND DRY SAUCE CHILI SRIRACHA CHA Food Costs Dry Goods Costs 50000 51500
536 534 CANNED AND DRY OLIVE KALAMATA PTD PLAS KEG Food Costs 50000
537 535 CANNED AND DRY MUSTARD YELLOW PRPD Food Costs 50000
538 536 CHEMICAL/JANTRL CLEANER DEGRSR GREASELIFT RTU Food Costs 50000
539 537 CANNED AND DRY SALT PKT .6 GM Food Costs 50000
540 538 CANNED AND DRY SPICE PEPPER PACKET .1 GM Food Costs Dry Goods Costs 50000 51500
541 539 PAPER & DISP LINER ROLL COMPOST47X60 1ML Paper Costs 55000
542 540 CANNED AND DRY WATER SPRKLG ORG ARANCAT CAN Food Costs 50000
543 541 DISPENSER BEVRG SYRUP COKE DIET 5X1 BIB Soft Beverage Costs 52000
564 562 CHEMICAL/JANTRL BLEACH LIQUID DISINFECT CLENER Food Costs 50000
565 563 PAPER & DISP CONTAINER PAPER FBR 9X6 1CPFF Paper Costs 55000
566 564 CANNED AND DRY SODA COKE MEXICO GLASS NON RET Soft Beverage Costs 52000
567 565 CANNED AND DRY SPICE CINNAMON STICK Food Costs Dry Goods Costs 50000 51500
568 566 CANNED AND DRY WALNUT HALVES AND PCS Food Costs 50000
569 567 DISPENSER BEVRG TEA ICED CONC RASP 5.5+1 Soft Beverage Costs 52000
570 568 MEATS BEEF GRND BULK 81/19 CHUB FRS Beef/Pork Costs 51110
658 656 PAPER & DISP CUP PLAS TRANS 16OZ SOFT Paper Costs 55000
659 657 PRODUCE PARSLEY BUNCH FDSVC Produce Costs 51200
660 658 PAPER & DISP LINER TRASH 24X32 .5 ML BLK Paper Costs 55000
661 659 CANNED AND DRY SPICE CINNAMON GRND Food Costs Dry Goods Costs 50000 51500
662 660 PAPER & DISP FORK PLAS HVY STY BLK Paper Costs 55000
663 661 PAPER & DISP SPOON PLAS PP HVY BLK FULL LEN Paper Costs 55000
664 662 DAIRY PRODUCTS EGG SHELL LG PAST CF Dairy Costs 51300
797 795 PAPER & DISP SUPPLY ACCESSORIES SOTF COM Paper Costs 55000
798 796 PAPER & DISP LID PLAS CLR FLT W/SLT 12-24OZ Paper Costs 55000
799 797 PAPER & DISP TRAY PAPER PULP CARRYOUT 4 CUP Paper Costs 55000
800 798 CHEMICAL/JANTRL SANITIZER OASIS 146 MULTI QUAT Food Costs Cleaning Supplies 50000 74100
801 799 PAPER & DISP CUP PLAS CLR TALL 8OZ RIGID Paper Costs 55000
802 800 PAPER & DISP BILLING MISC DISP Paper Costs 55000
803 801 PAPER & DISP LID FOIL F/ HALF STM TBL PAN Paper Costs 55000
816 814 CANNED AND DRY FLOUR SEMOLINA UNBLCH Food Costs 50000
817 815 CANNED AND DRY SAUCE HOT SRIRACHA BLUE AGAVE Food Costs 50000
818 816 PAPER & DISP LINER TRASH 43X48 16 MC NAT Paper Costs 55000
819 817 CANNED AND DRY WALNUT HALF & PCS Food Costs Produce Costs 50000 51200
820 818 MEATS BEEF SHORT RIB ASIAN CUT 1/4 Beef/Pork Costs 51110
821 819 PAPER & DISP LINER PLAS INSERT/WARMER 18X14 Paper Costs 55000
822 820 PAPER & DISP BOX PIZZA 14 W/K B-FLT 1-7/8 Paper Costs 55000
877 875 DAIRY PRODUCTS CHEESE CREAM LIGHT CUP Dairy Costs 51300
878 876 DAIRY PRODUCTS YOGURT BLUEBERRY GREEK NON FAT Dairy Costs 51300
879 877 HLTHCAR/HOSPITALITY PERKS MEMBERSHIP FEE Food Costs 50000
880 878 CANNED AND DRY DRESSING RED WINE VINGRT METRO Wine Costs Dressing & Sauce Cost 54400 51450
881 879 CANNED AND DRY SPICE GARLIC PWDR Food Costs 50000
882 880 CANNED AND DRY SPICE ONION POWDER Food Costs 50000
883 881 CANNED AND DRY SALT KOSHER FLAKE COARSE Food Costs 50000
925 923 CANNED AND DRY SAUCE WORCESTERSHIRE Food Costs 50000
926 924 PAPER & DISP CONTAINER PLAS 1C HNG 6X6 WHT Paper Costs 55000
927 925 PRODUCE PINEAPPLE FRESH Produce Costs 51200
928 926 SUPP & EQUIP MOP HEAD CTN CUT END VALUE #24 Food Costs Cleaning Supplies 50000 74100
929 927 DAIRY PRODUCTS CHEESE RICOTTA WMHM SEL Dairy Costs 51300
930 928 MEATS BACON LAYFLAT NT CC 13/17 PR12 Beef/Pork Costs 51110
931 929 CANNED AND DRY COOKIE CRUMB OREO MED CRUNCH Food Costs 50000
1005 1004 DAIRY PRODUCTS CHEESE BLUE STUFFED OLIVES Dairy Costs 51300
1006 1005 SUPP & EQUIP HANDLE MOP FIBRGLS QUICK CHNGE Food Costs 50000
1007 1006 SUPP & EQUIP SUPPLY SOTF JANSAN Food Costs 50000
1008 1007 CANNED AND DRY WALNUT HALVES & PCS Food Costs Produce Costs 50000 51200
1009 1008 CANNED AND DRY WATER SPARKLN ORG PRCKLY PEAR Beverages Costs 52000
1010 1009 CANNED AND DRY DRINK NATURAL CLMTN SPRKLG Food Costs 50000
1011 1010 CANNED AND DRY DRESSING MIX RNCH BTRMK NO MSG Food Costs 50000
1032 1031 CANNED AND DRY DRESSING BALSAMIC VINGT GARLIC Food Costs 50000
1033 1032 CANNED AND DRY SPREAD CHOC NUTELLA JAR FDSRV Food Costs 50000
1034 1033 CANNED AND DRY SUGAR BROWN LIGHT Food Costs 50000
1035 1034 PAPER & DISP PAD SCOUR 6X9 HVYDTY ANTIMICRO Paper Costs Cleaning Supplies 55000 74100
1036 1035 CHEMICAL/JANTRL DETERGENT POT/PAN LIQ GRN RTU Food Costs 50000
1037 1036 PAPER & DISP KNIFE PLAS HVY STY BLK Paper Costs 55000
1038 1037 CHEMICAL/JANTRL CLEANER DISINFECT PEROX RTU Food Costs 50000
1111 1110 PAPER & DISP BAG PAPER BRN W/HNDL REGAL 65# Paper Costs 55000
1112 1111 DAIRY PRODUCTS EGG SHELL WHT CAGEFREE GR A LG Dairy Costs 51300
1113 1112 CANNED AND DRY VINEGAR RICE SEASONED Food Costs 50000
1114 1113 CANNED AND DRY VINEGAR WHITE DSTD 5% Food Costs Dry Goods Costs 50000 51500
1115 1114 SEAFOOD SHRIMP WHT GH 13-15 Seafood Costs 51130
1116 1115 PAPER & DISP BAG PLAS PRTN 6.5X7 ORG SAT Paper Costs 55000
1117 1116 CANNED AND DRY BREAD CRUMB JAP PANKO TOASTED Food Costs 50000
1489 1488 POULTRY CHICKEN CVP WHL WOG FZ Chicken/ Poultry Costs 51120
1490 1489 PRODUCE LEEK BUNCH FRSH ICELS Produce Costs 51200
1491 1490 CANNED AND DRY BILLING MISC CANNED/DRY Food Costs 50000
1492 1491 PAPER & DISP PAN FOIL STEAM TBL HALF DEEP Paper Costs Dry Goods Costs 55000 51500
1493 1492 PAPER & DISP TRAY PAPER CARRIER 4 CUP Paper Costs 55000
1494 1493 PAPER & DISP KIT CUTLERY FKS/SP/NP HW PP BK Paper Costs 55000
1495 1494 MEATS BEEF PATTY 80/20 RND FRSH Beef/Pork Costs 51110
1498 1497 DAIRY PRODUCTS EGG HARDCOOKED CGFREE HARD PK Dairy Costs 51300
1499 1498 SEAFOOD SHRIMP WHT P&D TLOF 26/30 Seafood Costs 51130
1500 1499 PAPER & DISP CONTAINER PAPER HNG 9X6 1C FBR Paper Costs 55000
1501 1500 CHEMICAL/JANTRL DETERGENT POT & PAN LIQUID Food Costs Cleaning Supplies 50000 74100
1502 1501 FROZEN ASPARAGUS SPEAR MED IQF P Produce Costs 51200
1503 1502 PRODUCE ASPARAGUS FRESH STANDARD Produce Costs 51200
1504 1503 SUPP & EQUIP SCREEN GRIDDLE 4X6IN Food Costs 50000
1515 1514 PAPER & DISP GLOVE SYNTHETIC FDSRV PF MED Paper Costs 55000
1516 1515 POULTRY CHICKEN THIGH BNLS SKIN-ON RAW Chicken/ Poultry Costs 51120
1517 1516 CANNED AND DRY SPICE SAGE GRND Food Costs 50000
1518 1517 CANNED AND DRY KETCHUP SQUEEZE RED UPSIDE DWN Food Costs Dry Goods Costs 50000 51500
1519 1518 CANNED AND DRY SPICE NUTMEG WHL Food Costs 50000
1520 1519 DAIRY PRODUCTS YOGURT PLAIN FULL FAT Dairy Costs 51300
1521 1520 CANNED AND DRY VINEGAR WINE RED ITALY 6% GLS Wine Costs 54400
1555 1554 FROZEN BAKLAVA WALNT TRIANGLES Food Costs 50000
1556 1555 PAPER & DISP DISPENSER NAP XPRSNP STND BLK Paper Costs 55000
1557 1556 PAPER & DISP DISPENSER TOWEL MANUL COMP360 Paper Costs 55000
1558 1557 PAPER & DISP PAN FOIL STM TBL MED 2-3/16 Paper Costs Dry Goods Costs 55000 51500
1559 1558 PRODUCE ONION RED JUMBO CTN Produce Costs 51200
1560 1559 MEATS BEEF CHUCK SHORTRIB KOREAN1/2 Beef/Pork Costs 51110
1561 1560 FROZEN RICE MEXICAN STY Food Costs 50000
1653 1652 CANNED AND DRY SODA COKE CHERRY ZERO CONTOUR Soft Beverage Costs 52000
1654 1653 FROZEN ENTREE VEG FALAFEL BALLS VEGAN Food Costs 50000
1655 1654 SUPP & EQUIP BRUSH GRILL W/SCRPR 27 IN HNDL Food Costs 50000
1656 1655 CANNED AND DRY SAUCE HOT SRIRACHA HUY FONG Food Costs Dry Goods Costs 50000 51500
1657 1656 PAPER & DISP TOWEL MULTIFOLD PRM LEAF Paper Costs 55000
1658 1657 CANNED AND DRY SODA LEMON LIME 12OZ Soft Beverage Costs 52000
1659 1658 PAPER & DISP KNIFE PLAS WRP BLK Paper Costs 55000
1709 1708 PAPER & DISP CONTAINER PLAS CLR HNG 8IN Paper Costs 55000
1710 1709 PAPER & DISP TISSUE TOILET 2PL ADVC WHT WR Paper Costs 55000
1711 1710 SUPP & EQUIP SPATULA RUBBER SILICONE 10.25 Food Costs 50000
1712 1711 CANNED AND DRY BEAN GARBANZO FCY NO SULFITE Food Costs Dry Goods Costs 50000 51500
1713 1712 MEATS BACON SLI APLWD 13/17CT PR12 Beef/Pork Costs 51110
1714 1713 POULTRY SAUSAGE CHICKEN APPLE RAW 1 OZ Poultry Costs 51120
1715 1714 CANNED AND DRY TOMATO SUNDRIED JULENNE Food Costs 50000
1759 1758 MEATS PORK BELLY SKIN ON P12 COV Beef/Pork Costs 51110
1760 1759 MEATS PORK SHANK BONE KUROBUTA PR12 Beef/Pork Costs 51110
1761 1760 CANNED AND DRY SEASONING ITALIAN WHL Food Costs 50000
1762 1761 PRODUCE MUSHROOM PORTABELLA CAP 4-5 Produce Costs 51200
1763 1762 PAPER & DISP BAG PAPER 250 CT Paper Costs 55000
1764 1763 MEATS BEEF SHLDR TERES MAJOR SEL Beef/Pork Costs 51110
1765 1764 PAPER & DISP BOWL PLASTIC COATING 42 OZ Paper Costs 55000
1766 1765 PAPER & DISP BOX CATERING 21X13X4.25 LOGO Paper Costs 55000
1767 1766 CANNED AND DRY CANDY MILK CHOC SHELLS Dry Goods Costs 51500
1768 1767 CANNED AND DRY CHOCOLATE DUBAI PISTCHO KUNFEH Dry Goods Costs 51500
1769 1768 PAPER & DISP CONTAINER PAPER 1/30 OZ NTG Paper Costs 55000
1770 1769 PAPER & DISP CONTAINER PAPER 4/110OZ NTG Paper Costs 55000
1771 1770 PAPER & DISP CUP PAPER COLD 22 OZ LOGO NTG Paper Costs 55000
1772 1771 PAPER & DISP CUP PORTION PLAS CLR 1.50 OZ Paper Costs 55000
1773 1772 PAPER & DISP DESSERT CUP Paper Costs 55000
1774 1773 FROZEN DESSERT MINI PLAIN BEIGNET Bread and Bun Costs 51400
1775 1774 CANNED AND DRY DIP GARLIC TOUM Dressing & Sauce Cost 51450
1776 1775 CANNED AND DRY DRINK ENERGY ORANGE SPRKLNG Soft Beverage Costs 52000
1777 1776 CANNED AND DRY DRINK ENERGY PEACH VIBE SPRKLG Soft Beverage Costs 52000
1778 1777 CANNED AND DRY DRINK ENERGY TROPICAL VIBE Soft Beverage Costs 52000
1779 1778 PAPER & DISP FILM PVC 18X2000 ROLL Paper Costs 55000
1780 1779 CANNED AND DRY JUICE CONC MANDARIN CARDAMOM Soft Beverage Cost 52000
1781 1780 CANNED AND DRY JUICE CONC STRAWB DRAGON Soft Beverage Cost 52000
1782 1781 PAPER & DISP LID CLEAR PET 42 OZ Paper Costs 55000
1783 1782 PAPER & DISP LID DOME DESSERT CUP Paper Costs 55000
1784 1783 PAPER & DISP NAPKIN 2PLY INTR FOLD 6.3X8.26 Paper Costs 55000
1785 1784 CANNED AND DRY PASTE HERB HARISSA MOROCCAN Dressing & Sauce Cost 51450
1786 1785 CANNED AND DRY PASTE TAHINI DRESSING Dressing & Sauce Cost 51450
1787 1786 FROZEN PASTRY BEIGNET MN FLD CHOCCRML Bread and Bun Costs 51400
1788 1787 CANNED AND DRY PEPPER BANANA MILD RING Produce Costs 51200
1789 1788 CANNED AND DRY RICE MIX NICKS Dry Goods Costs 51500
1790 1789 CANNED AND DRY SODA CHERRY VISSINADA GREEK Soft Beverage Costs 52000
1791 1790 CANNED AND DRY SODA COLA PEPSI ZERO SUGAR Soft Beverage Costs 52000
1792 1791 CANNED AND DRY SODA PEPSI COLA Soft Beverage Costs 52000
1793 1792 FROZEN SPANAKOPITA SPINACH COOKED Bread and Bun Costs 51400
1794 1793 PAPER & DISP SPOON PLAS TEA PP X-HVY BLK Paper Costs 55000
1795 1794 PAPER & DISP WRAP PAPER 14X14 LOGO VER2 Paper Costs 55000
1796 1795 DAIRY PRODUCTS YOGURT FRZN NF NICK THE GREEK Dairy Costs 51300
1797 1796 FROZEN BALL FALAFEL FRTTR 1 OZ IQF Dry Goods Costs 51500
1798 1797 SUPP & EQUIP BASKET PLAS 10.5X7X1.5 BLK Paperware Cost 55000
1799 1798 CANNED AND DRY BEAN GARBANZO LOW SODIUM Dry Goods Costs 51500
1800 1799 FROZEN BREAD POTATO ROLL 4 INCH Bread and Bun Costs 51400
1801 1800 FROZEN BUN HAMBURGER 4IN 1.75 OZ Bread and Bun Costs 51400
1802 1801 DAIRY PRODUCTS CHEESE FETA CRUMBLES Dairy Costs 51300
1803 1802 FROZEN CHEESE STICK HALLOUMI STYL Dairy Costs 51300
1804 1803 POULTRY CHICKEN CVP THGH B/S HALAL Chicken/ Poultry Costs 51120
1805 1804 CHEMICAL/JANTRL CLEANER DEGREASER CONCENTR RTU Cleaning Supplies 74100
1806 1805 CHEMICAL/JANTRL CLEANER DEGREASER GRSELFT RTU Cleaning Supplies 74100
1807 1806 CONTAINER PAPER CUSTOM LOGO8X5 Paperware Cost 55000
1808 1807 CONTAINER PAPER FBR 9X6 1C WHT Paperware Cost 55000
1809 1808 DRESSING RANCH SPICY Dairy Costs 51300
1810 1809 FILM PVC ROLL CRYS 2000 FT Paperware Cost 55000
1811 1810 FORK PLASTIC SERVING BLK 10IN Paperware Cost 55000
1812 1811 DAIRY PRODUCTS ICE CREAM COOKIE&CREAM Dairy Costs 51300
1813 1812 DAIRY PRODUCTS ICE CREAM STRAWBERRY Dairy Costs 51300
1814 1813 DAIRY PRODUCTS ICE CREAM VAN QUICK BLEND Dairy Costs 51300
1815 1814 DISPENSER BEVRG JUICE CONC BERRY PATCH ORG Soft Beverage Cost 52000
1816 1815 CANNED AND DRY JUICE LEMON PLAS RTU Produce Costs 51200
1817 1816 PRODUCE KALE CHOPPED Produce Costs 51200
1818 1817 PRODUCE KALE FRESH Produce Costs 51200
1819 1818 CANNED AND DRY KETCHUP FCY POUCH EQUALS 6/10# Dry Goods Costs 51500
1820 1819 PRODUCE LEMON FRESH BAGGED Produce Costs 51200
1821 1820 CANNED AND DRY OIL OLIVE SOYBEAN BLEND 75/25 Dry Goods Costs 51500
1822 1821 PRODUCE ONION WHITE JUMBO BAG Produce Costs 51200
1823 1822 CANNED AND DRY PEPPER BANANA MILD RING 7-9HUN Produce Costs 51200
1824 1823 CANNED AND DRY PEPPER BANANA RING Produce Costs 51200
1825 1824 FROZEN POTATO FRY 1/4 SS XLF PHANTM Produce Costs 51200
1826 1825 SANITIZER NO RINSE QUORUM Cleaning Supplies 74100
1827 1826 CANNED AND DRY SODA ROOT BEER CUBE Soft Beverage Cost 52000
1828 1827 DISPENSER BEVRG SYRUP FRUIT PUNCH BIB FLASHIN Soft Beverage Cost 52000
1829 1828 DISPENSER BEVRG SYRUP ORANGE 5X1 BIB Soft Beverage Cost 52000
1830 1829 PRODUCE TOMATO BULK 5X5 FRSH Produce Costs 51200
1831 1830 PRODUCE TOMATO GRAPE FRSH Produce Costs 51200

View File

@@ -0,0 +1,43 @@
,,,,,,,,,,,,,,,,,,,,,,,,,d,,,,,,,,,,,,,,,,,,,,,,,,,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,20.56,CA,20.56,Y,0,0,0,1.2,07,48,02,01,CANNED AND DRY,24,20OZ,AQUAFIN,WATER PURIFIED BTL PET LSE DW,30,33,0.75,24,,000000,,0000,,,,29115,47,,8492330,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,14.84,CA,14.84,Y,0,0,0,0.6,07,16,05,01,CANNED AND DRY,12,11.2OZ,LOUX,SODA CHERRY VISSINADA GRK PLAS,9.5,10.5,0.26,12,,000000,,0000,,,3000P,808959,01,,7189422,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,14.93,CA,14.93,Y,0,0,0,0.6,07,16,05,01,CANNED AND DRY,12,8 OZ,LOUX,SODA LEMON LEMONADA GREEK,9,11.5,0.26,12,,000000,,0000,,,3200,808959,01,,9910355,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,79.83,CA,79.83,Y,0,0,0,0,07,35,03,99,CANNED AND DRY,4,5 LB,OTHRYS,SPICE OREGANO LEAF RUBBED,20,22,2.51,4,,000000,,0000,,,62760,808959,01,,9911236,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,31.07,CA,31.07,Y,0,0,0,0,07,35,99,99,CANNED AND DRY,22,4.68OZ,HI WEST,RICE MIX NICKS,6.43,7,0.16,22,,000000,,0000,,,30-5729,345717,03,,7301949,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,54.84,CA,109.68,Y,0,0,0,0,07,33,01,99,CANNED AND DRY,2,20 LB,ROYAL,RICE BASMATI PABROIL SELA CS,40,40.6,1.21,2,,000000,,0000,,,91000244,26992,43,,7053293,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,75.49,CA,75.49,Y,0,0,0,0,07,34,04,99,CANNED AND DRY,4,1 GAL,NICKGRK,DRESSING VINAIGRETTE LOGO,33,35,0.87,4,,000000,,0000,,,1654,853,01,,7108399,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,29.4,CA,29.4,Y,0,0,0,0,07,36,99,99,CANNED AND DRY,8,15 OZ,HAIG'S,DIP GARLIC TOUM,7.25,8.25,0.34,8,,000000,,0000,,,8PGD16,691816,01,,7360056,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,38.85,CA,38.85,Y,0,0,0,0,07,37,02,02,CANNED AND DRY,1,35 LB,BEOCO,OIL CORN,35,36.55,0.85,1,,000000,,0000,,,,9846,02,,4823761,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,56.72,CA,113.44,Y,0,0,0,0,07,36,99,99,CANNED AND DRY,4,4 LB,GRECDEL,SPREAD HUMMUS TRADITIONAL,16,17,0.62,4,,000000,,0000,,,HU000083,1533,19,,7278619,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,-7.22,CA,-7.32,Y,0,0,0,0,07,86,01,99,CANNED AND DRY,1,EA,NONPROD,ALLOWANCE FOR DROP SIZE,0.01,0.01,0.01,1,,000000,,0000,,,,,01,,9477498,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,4.17,CA,4.17,Y,0,0,0,0,07,86,01,99,CANNED AND DRY,1,EA,NONPROD,CHGS FOR FUEL SURCHARGE,1,1,0,1,,000000,,0000,,,,,01,,6592893,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,95.98,CA,191.96,Y,0,0,0,0,02,04,99,99,DAIRY PRODUCTS,1,5 GAL,NICKGRK,SAUCE TZATZIKI,42,43.5,1.2,1,,000000,,0000,,,SA000084,1533,19,,7213639,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,54.82,CA,109.64,Y,0,0,0,0,02,10,01,99,DAIRY PRODUCTS,4,1 GAL,NICKGRK,YOGURT FRZN NF NICK THE GREEK,39.9,39.9,0.97,4,,000000,,0000,,,13101,379887,05,,7302646,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,172.37,CA,172.37,Y,0,0,0,0,12,08,02,03,DISPENSER BEVRG,12,32 OZ,TRACTOR,JUICE CONC STRAWB DRAGON,24,25.5,0.58,12,,000000,,0000,,,6555,693956,01,,7206974,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,62.48,CA,62.48,Y,0,0,0,0,12,08,02,03,DISPENSER BEVRG,1,2.5GAL,DR PEPR,SYRUP DR PPR DIET BIB,20.93,21.82,0.47,1,,000000,,0000,,,12115,376510,09,,7459969,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,117.3,CA,117.3,Y,0,0,0,0,12,08,02,03,DISPENSER BEVRG,1,5GAL,DR PEPR,SYRUP DR PEPPER BIB,40,54.4,0.83,1,,000000,,0000,,,12109,9562,14,,4273553,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,8,8,0,26.36,CA,210.88,Y,0,0,0,0,06,02,45,99,FROZEN,12,10 CT,KONTOS,BREAD PITA GYRO PRE-OILED 7,21,24,1.65,12,,000000,,0000,,,10005,25370,01,,5223334,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,69.37,CA,138.74,Y,0,0,0,0,06,01,70,99,FROZEN,36,6 OZ,HELLAS,SPANAKOPITA SPINACH COOKED,12.4,13.4,0.62,36,,000000,,0000,,,216312,32248,01,,7455027,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,49.53,CA,99.06,Y,0,0,0,0,06,01,60,99,FROZEN,2,24 CT,HELLAS,BAKLAVA CLASSIC 2X24,9.6,10.6,0.46,2,,000000,,0000,,,100224,32248,01,,7187055,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,57.66,CA,57.66,Y,0,0,0,0,06,01,65,99,FROZEN,140,0.7 OZ,CHICPAT,DESSERT MINI PLAIN BEIGNET,6.17,7.5,0.88,140,,000000,,0000,,,540061,1188,53,,7212299,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,97.94,CA,97.94,Y,0,0,0,0,06,02,01,99,FROZEN,4,10 LB,NICKGRK,APTZR VEG FALAFEL PUCK HALAL,40,42,2.03,4,,000000,,0000,,,FA000090,1533,05,,7274591,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,1,53.5,1,53.5,7.556,LB,404.25,Y,0,0,0,0,03,02,01,13,MEATS,5,10.5#,TWORVRS,BEEF SHLDR TERES MAJOR SEL,53,55,1.99,5,,000000,,0000,,,B83003,527004,03,,0932867,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,87.76,CA,87.76,Y,0,0,0,0,03,04,99,99,MEATS,1,20 LB,GRECDEL,PORK SLI GYRO CONE,20,21,0.77,1,,000000,,0000,,,ME000215,1533,05,,7211838,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,7,7,0,92.53,CA,647.71,Y,0,0,0,0,03,02,04,99,MEATS,1,30 LB,NICKGRK,MEAT GYRO BEEF CONE NTG,30,31,0.97,1,,000000,,0000,,,ME000071,1533,05,,9906087,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,25.41,CA,25.41,Y,0,0,0,0,08,42,64,43,PAPER & DISP,20,50 CT,KARAT,LID PLAS FLAT F/12-22 OZ,5.75,7,1.94,20,,000000,,0000,,,C-KCL90,461672,05,,7661388,00000000000000,,,260402,04671945,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,22.32,CA,22.32,Y,0,0,0,0,08,60,99,99,PAPER & DISP,24,250 CT,ELEMEN,NAPKIN 2PLY INTR FOLD 6.3X8.26,16.1,16.8,1.55,24,,000000,,0000,,,11904,613310,01,,7452585,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,26.15,CA,26.15,Y,0,0,0,0,08,21,64,62,PAPER & DISP,50,50CT,KARAT,CUP PORTION PLAS CLR 1.50 OZ,10,10,1.36,50,,000000,,0000,,,FP-P150-PP,461672,05,,4613026,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,43.99,CA,43.99,Y,0,0,0,0,08,75,03,04,PAPER & DISP,6,50 EA,NATZWAY,BOWL PLASTIC COATING 42 OZ,14.55,17.19,3.56,6,,000000,,0000,,,10205,773772,01,,7408008,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,41.25,CA,41.25,Y,0,0,0,0,08,42,99,99,PAPER & DISP,6,50CT,NATZWAY,LID CLEAR PET 42 OZ,8.59,10.47,2.01,6,,000000,,0000,,,10206,773772,01,,7408215,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,47.6,CA,47.6,Y,0,0,0,0,08,21,56,99,PAPER & DISP,1000,22 OZ,NICKGRK,CUP PAPER COLD 22 OZ LOGO NTG,31.96,34.62,3.78,1000,,000000,,0000,,,810161542703,461672,05,,7354127,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,30.6,CA,30.6,Y,0,0,0,0,08,18,56,99,PAPER & DISP,1,450 CT,NICKGRK,CONTAINER PAPER 1/30 OZ NTG,23,25,3.25,1,,000000,,0000,,,810161542673,461672,05,,7354120,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,27.6,CA,27.6,Y,0,0,0,0,08,18,56,99,PAPER & DISP,1,160 CT,NICKGRK,CONTAINER PAPER 4/110OZ NTG,19.6,21.4,3.59,1,,000000,,0000,,,810161542680,461672,05,,7354119,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,3,3,0,37.6,CA,112.8,Y,0,0,0,0,08,18,02,12,PAPER & DISP,2,100CT,NATZWAY,CONTAINER PAPER MLD FBR 9X6,18,18,1.28,2,,000000,,0000,,,10042,773772,01,,7250678,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,46.07,CA,92.14,Y,0,0,0,0,08,09,56,99,PAPER & DISP,1,250BAG,NICKGRK,BAG PAPER 250 CT,14.5,15,2.18,1,,000000,,0000,,,,773772,01,,7417242,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,1,,1,1,0,24.46,EA,24.46,Y,0,0,0,0,08,36,56,79,PAPER & DISP,5,1000,BAGCRFT,WRAP DELI WHT 12X12 GRS RESIST,37,37,1.07,5,,000000,,0000,,,P057012,276,01,,5723808,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,39.41,CA,39.41,Y,3.85,0,0,0,08,06,25,10,PAPER & DISP,10,100CT,DHGPROF,GLOVE NITRILE BLK PEDRFREE LRG,12.21,12.21,0.66,10,,000000,,0000,,,DNGB-L,613310,01,,7296407,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,3,3,0,89.32,CA,267.96,Y,0,0,0,0,05,01,01,07,POULTRY,4,10 LB,SYS CLS,CHICKEN CVP THIGH BNLS SKLS,40,42,1.04,4,,000000,,0000,,,14301,3254,21,,7792187,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,9,9,0,86.97,CA,782.73,Y,0,0,0,0,05,02,01,99,POULTRY,1,20LB,GRECDEL,GYRO CHICKEN SHAWARMA CONE,20,21,0.77,1,,000000,,0000,,,ME000102,1533,05,,7124188,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,5,5,0,23.65,CA,118.25,Y,0,0,0,0,11,02,23,01,PRODUCE,1,50 LB,PACKER,POTATO KENNEBEC FRESH,50,52,2,1,,000000,,0000,,,,696760,01,,2039220,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,HDR,,,CKC CONCORD INC,,260402,,,,Rolling 8,,NICK THE GREEK CONCORD,2075 DIAMOND BLVD,STE H-103,CONCORD,CA,94520-582,408593,000000000,,,,,BBNKG,0,050,SYSCO SAN FRANCISCO,5900 STEWART AVENU,,FREMONT,CA,94538,,,,,,,1372486,4024,004,0000000,00000000,20260529,6.25,CRO8
EEK,,050,00175469,850081745,SUM,,,40,0,0,74,0,4625.36,6.25,00000463161,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1 d
2 EEK 050 00175469 850081745 DET 1 1 0 20.56 CA 20.56 Y 0 0 0 1.2 07 48 02 01 CANNED AND DRY 24 20OZ AQUAFIN WATER PURIFIED BTL PET LSE DW 30 33 0.75 24 000000 0000 29115 47 8492330 00000000000000 260402 04672959
3 EEK 050 00175469 850081745 DET 1 1 0 14.84 CA 14.84 Y 0 0 0 0.6 07 16 05 01 CANNED AND DRY 12 11.2OZ LOUX SODA CHERRY VISSINADA GRK PLAS 9.5 10.5 0.26 12 000000 0000 3000P 808959 01 7189422 00000000000000 260402 04672959
4 EEK 050 00175469 850081745 DET 1 1 0 14.93 CA 14.93 Y 0 0 0 0.6 07 16 05 01 CANNED AND DRY 12 8 OZ LOUX SODA LEMON LEMONADA GREEK 9 11.5 0.26 12 000000 0000 3200 808959 01 9910355 00000000000000 260402 04672959
5 EEK 050 00175469 850081745 DET 1 1 0 79.83 CA 79.83 Y 0 0 0 0 07 35 03 99 CANNED AND DRY 4 5 LB OTHRYS SPICE OREGANO LEAF RUBBED 20 22 2.51 4 000000 0000 62760 808959 01 9911236 00000000000000 260402 04672959
6 EEK 050 00175469 850081745 DET 1 1 0 31.07 CA 31.07 Y 0 0 0 0 07 35 99 99 CANNED AND DRY 22 4.68OZ HI WEST RICE MIX NICKS 6.43 7 0.16 22 000000 0000 30-5729 345717 03 7301949 00000000000000 260402 04672959
7 EEK 050 00175469 850081745 DET 2 2 0 54.84 CA 109.68 Y 0 0 0 0 07 33 01 99 CANNED AND DRY 2 20 LB ROYAL RICE BASMATI PABROIL SELA CS 40 40.6 1.21 2 000000 0000 91000244 26992 43 7053293 00000000000000 260402 04672959
8 EEK 050 00175469 850081745 DET 1 1 0 75.49 CA 75.49 Y 0 0 0 0 07 34 04 99 CANNED AND DRY 4 1 GAL NICKGRK DRESSING VINAIGRETTE LOGO 33 35 0.87 4 000000 0000 1654 853 01 7108399 00000000000000 260402 04672959
9 EEK 050 00175469 850081745 DET 1 1 0 29.4 CA 29.4 Y 0 0 0 0 07 36 99 99 CANNED AND DRY 8 15 OZ HAIG'S DIP GARLIC TOUM 7.25 8.25 0.34 8 000000 0000 8PGD16 691816 01 7360056 00000000000000 260402 04672959
10 EEK 050 00175469 850081745 DET 1 1 0 38.85 CA 38.85 Y 0 0 0 0 07 37 02 02 CANNED AND DRY 1 35 LB BEOCO OIL CORN 35 36.55 0.85 1 000000 0000 9846 02 4823761 00000000000000 260402 04672959
11 EEK 050 00175469 850081745 DET 2 2 0 56.72 CA 113.44 Y 0 0 0 0 07 36 99 99 CANNED AND DRY 4 4 LB GRECDEL SPREAD HUMMUS TRADITIONAL 16 17 0.62 4 000000 0000 HU000083 1533 19 7278619 00000000000000 260402 04672959
12 EEK 050 00175469 850081745 DET 1 1 0 -7.22 CA -7.32 Y 0 0 0 0 07 86 01 99 CANNED AND DRY 1 EA NONPROD ALLOWANCE FOR DROP SIZE 0.01 0.01 0.01 1 000000 0000 01 9477498 00000000000000 260402 04672959
13 EEK 050 00175469 850081745 DET 1 1 0 4.17 CA 4.17 Y 0 0 0 0 07 86 01 99 CANNED AND DRY 1 EA NONPROD CHGS FOR FUEL SURCHARGE 1 1 0 1 000000 0000 01 6592893 00000000000000 260402 04672959
14 EEK 050 00175469 850081745 DET 2 2 0 95.98 CA 191.96 Y 0 0 0 0 02 04 99 99 DAIRY PRODUCTS 1 5 GAL NICKGRK SAUCE TZATZIKI 42 43.5 1.2 1 000000 0000 SA000084 1533 19 7213639 00000000000000 260402 04672959
15 EEK 050 00175469 850081745 DET 2 2 0 54.82 CA 109.64 Y 0 0 0 0 02 10 01 99 DAIRY PRODUCTS 4 1 GAL NICKGRK YOGURT FRZN NF NICK THE GREEK 39.9 39.9 0.97 4 000000 0000 13101 379887 05 7302646 00000000000000 260402 04672959
16 EEK 050 00175469 850081745 DET 1 1 0 172.37 CA 172.37 Y 0 0 0 0 12 08 02 03 DISPENSER BEVRG 12 32 OZ TRACTOR JUICE CONC STRAWB DRAGON 24 25.5 0.58 12 000000 0000 6555 693956 01 7206974 00000000000000 260402 04672959
17 EEK 050 00175469 850081745 DET 1 1 0 62.48 CA 62.48 Y 0 0 0 0 12 08 02 03 DISPENSER BEVRG 1 2.5GAL DR PEPR SYRUP DR PPR DIET BIB 20.93 21.82 0.47 1 000000 0000 12115 376510 09 7459969 00000000000000 260402 04672959
18 EEK 050 00175469 850081745 DET 1 1 0 117.3 CA 117.3 Y 0 0 0 0 12 08 02 03 DISPENSER BEVRG 1 5GAL DR PEPR SYRUP DR PEPPER BIB 40 54.4 0.83 1 000000 0000 12109 9562 14 4273553 00000000000000 260402 04672959
19 EEK 050 00175469 850081745 DET 8 8 0 26.36 CA 210.88 Y 0 0 0 0 06 02 45 99 FROZEN 12 10 CT KONTOS BREAD PITA GYRO PRE-OILED 7 21 24 1.65 12 000000 0000 10005 25370 01 5223334 00000000000000 260402 04672959
20 EEK 050 00175469 850081745 DET 2 2 0 69.37 CA 138.74 Y 0 0 0 0 06 01 70 99 FROZEN 36 6 OZ HELLAS SPANAKOPITA SPINACH COOKED 12.4 13.4 0.62 36 000000 0000 216312 32248 01 7455027 00000000000000 260402 04672959
21 EEK 050 00175469 850081745 DET 2 2 0 49.53 CA 99.06 Y 0 0 0 0 06 01 60 99 FROZEN 2 24 CT HELLAS BAKLAVA CLASSIC 2X24 9.6 10.6 0.46 2 000000 0000 100224 32248 01 7187055 00000000000000 260402 04672959
22 EEK 050 00175469 850081745 DET 1 1 0 57.66 CA 57.66 Y 0 0 0 0 06 01 65 99 FROZEN 140 0.7 OZ CHICPAT DESSERT MINI PLAIN BEIGNET 6.17 7.5 0.88 140 000000 0000 540061 1188 53 7212299 00000000000000 260402 04672959
23 EEK 050 00175469 850081745 DET 1 1 0 97.94 CA 97.94 Y 0 0 0 0 06 02 01 99 FROZEN 4 10 LB NICKGRK APTZR VEG FALAFEL PUCK HALAL 40 42 2.03 4 000000 0000 FA000090 1533 05 7274591 00000000000000 260402 04672959
24 EEK 050 00175469 850081745 DET 1 53.5 1 53.5 7.556 LB 404.25 Y 0 0 0 0 03 02 01 13 MEATS 5 10.5# TWORVRS BEEF SHLDR TERES MAJOR SEL 53 55 1.99 5 000000 0000 B83003 527004 03 0932867 00000000000000 260402 04672959
25 EEK 050 00175469 850081745 DET 1 1 0 87.76 CA 87.76 Y 0 0 0 0 03 04 99 99 MEATS 1 20 LB GRECDEL PORK SLI GYRO CONE 20 21 0.77 1 000000 0000 ME000215 1533 05 7211838 00000000000000 260402 04672959
26 EEK 050 00175469 850081745 DET 7 7 0 92.53 CA 647.71 Y 0 0 0 0 03 02 04 99 MEATS 1 30 LB NICKGRK MEAT GYRO BEEF CONE NTG 30 31 0.97 1 000000 0000 ME000071 1533 05 9906087 00000000000000 260402 04672959
27 EEK 050 00175469 850081745 DET 1 1 0 25.41 CA 25.41 Y 0 0 0 0 08 42 64 43 PAPER & DISP 20 50 CT KARAT LID PLAS FLAT F/12-22 OZ 5.75 7 1.94 20 000000 0000 C-KCL90 461672 05 7661388 00000000000000 260402 04671945
28 EEK 050 00175469 850081745 DET 1 1 0 22.32 CA 22.32 Y 0 0 0 0 08 60 99 99 PAPER & DISP 24 250 CT ELEMEN NAPKIN 2PLY INTR FOLD 6.3X8.26 16.1 16.8 1.55 24 000000 0000 11904 613310 01 7452585 00000000000000 260402 04672959
29 EEK 050 00175469 850081745 DET 1 1 0 26.15 CA 26.15 Y 0 0 0 0 08 21 64 62 PAPER & DISP 50 50CT KARAT CUP PORTION PLAS CLR 1.50 OZ 10 10 1.36 50 000000 0000 FP-P150-PP 461672 05 4613026 00000000000000 260402 04672959
30 EEK 050 00175469 850081745 DET 1 1 0 43.99 CA 43.99 Y 0 0 0 0 08 75 03 04 PAPER & DISP 6 50 EA NATZWAY BOWL PLASTIC COATING 42 OZ 14.55 17.19 3.56 6 000000 0000 10205 773772 01 7408008 00000000000000 260402 04672959
31 EEK 050 00175469 850081745 DET 1 1 0 41.25 CA 41.25 Y 0 0 0 0 08 42 99 99 PAPER & DISP 6 50CT NATZWAY LID CLEAR PET 42 OZ 8.59 10.47 2.01 6 000000 0000 10206 773772 01 7408215 00000000000000 260402 04672959
32 EEK 050 00175469 850081745 DET 1 1 0 47.6 CA 47.6 Y 0 0 0 0 08 21 56 99 PAPER & DISP 1000 22 OZ NICKGRK CUP PAPER COLD 22 OZ LOGO NTG 31.96 34.62 3.78 1000 000000 0000 810161542703 461672 05 7354127 00000000000000 260402 04672959
33 EEK 050 00175469 850081745 DET 1 1 0 30.6 CA 30.6 Y 0 0 0 0 08 18 56 99 PAPER & DISP 1 450 CT NICKGRK CONTAINER PAPER 1/30 OZ NTG 23 25 3.25 1 000000 0000 810161542673 461672 05 7354120 00000000000000 260402 04672959
34 EEK 050 00175469 850081745 DET 1 1 0 27.6 CA 27.6 Y 0 0 0 0 08 18 56 99 PAPER & DISP 1 160 CT NICKGRK CONTAINER PAPER 4/110OZ NTG 19.6 21.4 3.59 1 000000 0000 810161542680 461672 05 7354119 00000000000000 260402 04672959
35 EEK 050 00175469 850081745 DET 3 3 0 37.6 CA 112.8 Y 0 0 0 0 08 18 02 12 PAPER & DISP 2 100CT NATZWAY CONTAINER PAPER MLD FBR 9X6 18 18 1.28 2 000000 0000 10042 773772 01 7250678 00000000000000 260402 04672959
36 EEK 050 00175469 850081745 DET 2 2 0 46.07 CA 92.14 Y 0 0 0 0 08 09 56 99 PAPER & DISP 1 250BAG NICKGRK BAG PAPER 250 CT 14.5 15 2.18 1 000000 0000 773772 01 7417242 00000000000000 260402 04672959
37 EEK 050 00175469 850081745 DET 1 1 1 0 24.46 EA 24.46 Y 0 0 0 0 08 36 56 79 PAPER & DISP 5 1000 BAGCRFT WRAP DELI WHT 12X12 GRS RESIST 37 37 1.07 5 000000 0000 P057012 276 01 5723808 00000000000000 260402 04672959
38 EEK 050 00175469 850081745 DET 1 1 0 39.41 CA 39.41 Y 3.85 0 0 0 08 06 25 10 PAPER & DISP 10 100CT DHGPROF GLOVE NITRILE BLK PEDRFREE LRG 12.21 12.21 0.66 10 000000 0000 DNGB-L 613310 01 7296407 00000000000000 260402 04672959
39 EEK 050 00175469 850081745 DET 3 3 0 89.32 CA 267.96 Y 0 0 0 0 05 01 01 07 POULTRY 4 10 LB SYS CLS CHICKEN CVP THIGH BNLS SKLS 40 42 1.04 4 000000 0000 14301 3254 21 7792187 00000000000000 260402 04672959
40 EEK 050 00175469 850081745 DET 9 9 0 86.97 CA 782.73 Y 0 0 0 0 05 02 01 99 POULTRY 1 20LB GRECDEL GYRO CHICKEN SHAWARMA CONE 20 21 0.77 1 000000 0000 ME000102 1533 05 7124188 00000000000000 260402 04672959
41 EEK 050 00175469 850081745 DET 5 5 0 23.65 CA 118.25 Y 0 0 0 0 11 02 23 01 PRODUCE 1 50 LB PACKER POTATO KENNEBEC FRESH 50 52 2 1 000000 0000 696760 01 2039220 00000000000000 260402 04672959
42 EEK 050 00175469 850081745 HDR CKC CONCORD INC 260402 Rolling 8 NICK THE GREEK CONCORD 2075 DIAMOND BLVD STE H-103 CONCORD CA 94520-582 408593 000000000 BBNKG 0 050 SYSCO SAN FRANCISCO 5900 STEWART AVENU FREMONT CA 94538 1372486 4024 004 0000000 00000000 20260529 6.25 CRO8
43 EEK 050 00175469 850081745 SUM 40 0 0 74 0 4625.36 6.25 00000463161

View File

@@ -0,0 +1,271 @@
;; =====================================================================
;; ONE-OFF SCRATCH — re-code already-imported Sysco invoices after fixing
;; resources/sysco_line_item_mapping.csv.
;;
;; Context: the Sysco importer codes each line item by EXACT description
;; match against sysco_line_item_mapping.csv, defaulting to GL 50000 when a
;; description is missing (auto-ap.jobs.sysco/get-line-account). Missing
;; PAPER & DISP (and other) descriptions landed in 50000 (Food Costs)
;; instead of their real accounts (e.g. 55000 Paper Costs). The mapping is
;; now fixed; this re-derives the correct split from each invoice's source
;; CSV and rewrites :invoice/expense-accounts.
;;
;; Design:
;; - Recode EVERY invoice found in the CSV resource (a Sysco file may batch
;; several invoices; they're grouped by InvoiceNumber).
;; - Build ONE transaction covering every invoice, emitting only the datoms
;; that actually change:
;; * reuse an existing invoice-expense-account when its account (and
;; location) already match, updating just :amount when it differs;
;; * add a child for an account that has no row yet;
;; * retract a child whose account is no longer in the corrected split;
;; * emit nothing for rows already correct.
;; - Validate with (dc/with db changes): apply the tx to an in-memory db
;; value and assert every affected invoice's expense-account amounts sum
;; to its :invoice/total BEFORE committing for real.
;; - After committing, touch the ledger for every affected invoice. This is
;; a SEPARATE transaction on purpose: :upsert-invoice rebuilds the journal
;; entry from the invoice's expense-accounts as seen in db-before, so it
;; must run after the recode is committed.
;;
;; DO NOT load/evaluate this whole file. Step through the (comment ...) forms
;; one at a time in a connected REPL; the commit + ledger steps are gated #_.
;;
;; PRECONDITIONS
;; - The deployed artifact ships the fixed sysco_line_item_mapping.csv AND
;; the invoice CSV at resources/sysco_recode/<file>.csv (io/resource).
;; - You are connected to the DB you intend to mutate (prod conn!).
;; =====================================================================
(comment
(require '[auto-ap.jobs.sysco :as sysco]
'[auto-ap.datomic :refer [conn audit-transact random-tempid]]
'[auto-ap.utils :refer [dollars=]]
'[auto-ap.time :as t]
'[clj-time.coerce :as coerce]
'[clojure.data.csv :as csv]
'[clojure.java.io :as io]
'[datomic.api :as dc])
;; ------------------------------------------------------------------
;; STEP 0 — reload the mapping cache so the corrected CSV is in effect.
;; ------------------------------------------------------------------
(reset! sysco/sysco-name->line nil)
(count (sysco/get-sysco->line))
;; sanity: a previously-missing paper description now resolves to 55000.
(dc/pull (dc/db conn) [:account/numeric-code :account/name]
(sysco/get-line-account "BAG PAPER 250 CT")) ; => 55000
;; ------------------------------------------------------------------
;; Helpers
;; ------------------------------------------------------------------
(defn read-csv-rows
"Reads the invoice CSV from the classpath (so it ships with the deploy).
`resource-path` is relative to a resources/ root, e.g. \"sysco_recode/bad.csv\"."
[resource-path]
(with-open [r (io/reader (or (io/resource resource-path)
(throw (ex-info "CSV not found on classpath"
{:resource-path resource-path}))))]
(doall (csv/read-csv r))))
(defn parse-date
"Sysco yyMMdd string -> java.util.Date, the same way the importer stores
:invoice/date (auto-ap.jobs.sysco/extract-invoice-details)."
[yymmdd]
(coerce/to-date (t/parse yymmdd "yyMMdd")))
(defn group-invoices
"Split a (possibly multi-invoice) Sysco CSV into one entry per invoice.
Groups DET/HDR/SUM rows by InvoiceNumber (index 4); date comes from the
group's HDR row InvoiceDate (index 10)."
[rows]
(->> rows
(filter #(contains? #{"DET" "HDR" "SUM"} (nth % 5 nil)))
(group-by #(nth % 4))
(mapv (fn [[number grp]]
(let [hdr (first (filter #(= "HDR" (nth % 5)) grp))]
{:invoice-number number
:date-str (some-> hdr (nth 10))
:rows grp})))))
(defn desired-split
"Rows of one Sysco invoice -> {account-eid -> amount-double}, using the
CURRENT (fixed) mapping. DET rows only (record-type at index 5); tax
(SUM row, TotalTaxAmount index 14) added to the same account the
importer uses for \"TAX\". Mirrors auto-ap.jobs.sysco/code-individual-items."
[rows]
(let [det (filter #(= "DET" (nth % 5)) rows)
sum-row (first (filter #(= "SUM" (nth % 5)) rows))
tax (some-> sum-row (nth 14) Double/parseDouble)
by-acct (reduce
(fn [acc row]
(update acc
(sysco/get-line-account (nth row sysco/item-name-index))
(fnil + 0.0)
(Double/parseDouble (nth row sysco/item-price-index))))
{}
det)]
(cond-> by-acct
(and tax (not (zero? tax)))
(update (sysco/get-line-account "TAX") (fnil + 0.0) tax))))
(defn resolve-eid
"Match on invoice-number AND date (belt-and-suspenders). Asserts a unique
hit so we never recode the wrong invoice."
[invoice-number date]
(let [ids (mapv first (dc/q '[:find ?i :in $ ?n ?d
:where
[?i :invoice/invoice-number ?n]
[?i :invoice/date ?d]]
(dc/db conn) invoice-number date))]
(assert (>= 1 (count ids))
(str "multiple invoices match " invoice-number " / " date ": " ids))
(first ids)))
(defn invoice-change-datoms
"Minimal tx-data to make invoice `eid`'s expense-account split equal
`desired` ({account-eid -> amount}). Returns [] when already correct."
[db eid desired]
(let [existing (:invoice/expense-accounts
(dc/pull db [{:invoice/expense-accounts
[:db/id :invoice-expense-account/amount
:invoice-expense-account/location
{:invoice-expense-account/account [:db/id]}]}]
eid))
loc (or (some :invoice-expense-account/location existing) "HQ")
;; one child per account expected; index by account, retract any dupes
by-acct (group-by #(get-in % [:invoice-expense-account/account :db/id]) existing)
one (into {} (map (fn [[a cs]] [a (first cs)])) by-acct)
dupes (mapcat (fn [[_ cs]] (map :db/id (rest cs))) by-acct)
wanted (set (keys desired))
upserts (keep (fn [[acct amt]]
(let [child (get one acct)]
(cond
;; new account -> accrete a child under the invoice
(nil? child)
{:db/id eid
:invoice/expense-accounts
[#:invoice-expense-account{:db/id (random-tempid)
:account acct
:location loc
:amount amt}]}
;; right account, wrong value -> reuse, set amount
;; (and fix location if it drifted)
(or (not (dollars= (:invoice-expense-account/amount child) amt))
(not= (:invoice-expense-account/location child) loc))
(cond-> {:db/id (:db/id child)
:invoice-expense-account/amount amt}
(not= (:invoice-expense-account/location child) loc)
(assoc :invoice-expense-account/location loc))
;; already correct -> nothing
:else nil)))
desired)
retracts (for [[acct child] one :when (not (wanted acct))]
[:db/retractEntity (:db/id child)])]
(vec (concat upserts
retracts
(map (fn [id] [:db/retractEntity id]) dupes)))))
;; ------------------------------------------------------------------
;; STEP 1 — point at the CSV. EVERY invoice in this file gets recoded.
;; Place the file under resources/ (e.g. resources/sysco_recode/bad.csv)
;; and commit it so it's on the classpath of the deployed artifact.
;; ------------------------------------------------------------------
(def csv-path "sysco_recode/bad.csv")
(def rows (read-csv-rows csv-path))
(def invoices (group-invoices rows))
(mapv (juxt :invoice-number :date-str) invoices) ;; what we found in the file
;; ------------------------------------------------------------------
;; STEP 2 — resolve each invoice (number + date) and compute its split.
;; ------------------------------------------------------------------
(def plan
(mapv (fn [{:keys [invoice-number date-str rows]}]
(let [date (parse-date date-str)]
{:invoice-number invoice-number
:date date
:eid (resolve-eid invoice-number date)
:desired (desired-split rows)}))
invoices))
;; bail if any invoice number didn't resolve
(assert (every? :eid plan)
(str "unresolved invoices: "
(mapv (juxt :invoice-number :date) (remove :eid plan))))
;; ------------------------------------------------------------------
;; STEP 3 — build the SINGLE changes-only transaction across all invoices.
;; ------------------------------------------------------------------
(def changes
(let [db (dc/db conn)]
(vec (mapcat (fn [{:keys [eid desired]}] (invoice-change-datoms db eid desired))
plan))))
(count changes) ;; how many datoms we're actually changing
changes ;; inspect the full minimal tx
;; ------------------------------------------------------------------
;; STEP 4 — validate with dc/with: apply the tx to an in-memory db value
;; and confirm every affected invoice still balances (sum of expense
;; account amounts == :invoice/total).
;; ------------------------------------------------------------------
(def preview (dc/with (dc/db conn) changes))
(def balance-report
(let [db-after (:db-after preview)]
(mapv (fn [{:keys [eid invoice-number]}]
(let [inv (dc/pull db-after
[:invoice/total
{:invoice/expense-accounts [:invoice-expense-account/amount]}]
eid)
s (reduce + 0.0 (map :invoice-expense-account/amount
(:invoice/expense-accounts inv)))]
{:invoice-number invoice-number
:total (:invoice/total inv)
:ea-sum s
:ok? (dollars= s (:invoice/total inv))}))
plan)))
balance-report
;; HARD GATE — do not continue unless every invoice balances post-change.
(assert (every? :ok? balance-report)
(str "unbalanced after change: "
(filterv (complement :ok?) balance-report)))
;; ------------------------------------------------------------------
;; STEP 5 — COMMIT the recode (gated). One transaction, changes only.
;; ------------------------------------------------------------------
#_(audit-transact changes
{:user/name "sysco recode (missing GL mappings fix)"
:user/role "admin"})
;; ------------------------------------------------------------------
;; STEP 6 — touch the ledger for every affected invoice (separate tx;
;; :upsert-invoice rebuilds the journal entry from the now-committed
;; expense-accounts). Gated.
;; ------------------------------------------------------------------
#_(audit-transact (mapv (fn [{:keys [eid]}] [:upsert-invoice {:db/id eid}]) plan)
{:user/name "sysco recode ledger touch"
:user/role "admin"})
;; ------------------------------------------------------------------
;; STEP 7 — verify committed result.
;; ------------------------------------------------------------------
#_(let [db (dc/db conn)]
(mapv (fn [{:keys [eid invoice-number]}]
{:invoice-number invoice-number
:accounts
(->> (dc/pull db
[{:invoice/expense-accounts
[:invoice-expense-account/amount
{:invoice-expense-account/account [:account/numeric-code]}]}]
eid)
:invoice/expense-accounts
(map (juxt #(get-in % [:invoice-expense-account/account :account/numeric-code])
:invoice-expense-account/amount))
(sort-by first)
vec)})
plan)))

View File

@@ -875,13 +875,22 @@
(defn all-schema []
(edn/read-string (slurp (io/resource "schema.edn"))))
(defn transact-schema [conn]
@(dc/transact conn
(edn/read-string (slurp (io/resource "schema.edn"))))
(defn transact-schema
"Installs the schema in two passes: every plain attribute first, then every composite tuple.
;; this is temporary for any new stuff that needs to be asserted for cloud migration.
@(dc/transact conn
(edn/read-string (slurp (io/resource "cloud-migration-schema.edn")))))
A tuple can only be created once the attributes it composes already exist, and the pieces are
spread across both files — `:journal-entry-line/running-balance-tuple` lives in schema.edn
while one of its members, `:journal-entry-line/running-balance`, lives in
cloud-migration-schema.edn. Transacting the files in order therefore cannot install that tuple
against an empty database. Long-lived databases never hit it because those attributes went in
years apart."
[conn]
(let [schema (concat (edn/read-string (slurp (io/resource "schema.edn")))
;; this is temporary for any new stuff that needs to be asserted for cloud migration.
(edn/read-string (slurp (io/resource "cloud-migration-schema.edn"))))
{tuples true plain false} (group-by #(contains? % :db/tupleAttrs) schema)]
(when (seq plain) @(dc/transact conn plain))
(when (seq tuples) @(dc/transact conn tuples))))
(defn backoff [n]
(let [base-timeout 500

View File

@@ -0,0 +1,62 @@
(ns auto-ap.datomic.sales-summaries
(:require
[iol-ion.query :refer [dollars=]]))
(defn- ledger-side
"The ledger side of an item as a keyword, whether it arrived as a plain keyword (a
transaction map, or a pull using `:xform iol-ion.query/ident`) or as the `{:db/ident ...}`
map a plain `pull` returns. Resolving both shapes here matters: items whose side does not
compare equal are counted on neither side, which would leave a summary looking balanced at
zero and therefore silently accepted."
[item]
(let [side (:ledger-mapped/ledger-side item)]
(if (map? side) (:db/ident side) side)))
(defn- side-total [side items]
(->> items
(filter #(= side (ledger-side %)))
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0)))
(defn total-debits [items]
(side-total :ledger-side/debit items))
(defn total-credits [items]
(side-total :ledger-side/credit items))
(defn fully-mapped? [items]
(every? :ledger-mapped/account items))
(defn fully-sided?
"Every item says which side of the ledger it belongs on. Guards `accepted?` against
reading a collection of sideless items as balanced at zero."
[items]
(every? #(#{:ledger-side/debit :ledger-side/credit} (ledger-side %)) items))
(defn balanced? [items]
(dollars= (total-debits items) (total-credits items)))
(defn imbalance
"Signed debits minus credits. `balanced?` answers yes or no; this says by how much and in
which direction, so a day that does not balance can be logged and queried rather than only
rendered red. Positive means the tender side exceeds what revenue accounts for."
[items]
(- (total-debits items) (total-credits items)))
(defn accepted?
"True once a summary is finished: every line is mapped to an account and debits equal
credits. This is the same condition the sales summaries grid renders as \"Balanced\", and
the condition the scheduled refresh treats as \"leave this alone\"."
[items]
(boolean (and (seq items)
(fully-mapped? items)
(fully-sided? items)
(balanced? items))))
(defn <-pulled-item
"Flattens the ref values on a pulled sales summary item back to the scalars a transaction
expects. `accepted?` reads either shape, so this is only needed on the write path."
[item]
(cond-> item
(map? (:ledger-mapped/ledger-side item)) (update :ledger-mapped/ledger-side :db/ident)
(map? (:ledger-mapped/account item)) (update :ledger-mapped/account :db/id)))

View File

@@ -0,0 +1,375 @@
(ns auto-ap.jobs.backfill-olo-processors
"One-off backfill for the Olo third-party-source fix (commit fa25620b, \"olo fixes\").
That commit replaced the exact-match `condp` on `(:name (:source order))` in
`square3/tender->charge` with substring matching, so Olo-brokered orders that
arrive from Square with source names like \"Olo - DoorDash\" or
\"olo-ubereats\" now classify as the real delivery processor instead of
falling through to `:ccp-processor/na`.
Orders imported *before* the fix shipped still carry the old
`:charge/processor`, which means `sales-summaries` bucketed them as
\"Unknown\" instead of \"Food App Payments\". This job recomputes
`:charge/processor` for already-imported Square charges over a date range
(default: 2026-07-07 -> today) and marks the affected sales summaries dirty
so `auto-ap.jobs.sales-summaries` recalculates them.
IMPORTANT: this backfill never re-implements the classifier. It feeds the
stored shape of each charge back through the real production function
(`square3/tender->charge`), so it cannot drift from the importer.
No Square API calls are made -- every input the classifier needs
(`:sales-order/source`, `:charge/note`, `:charge/type-name`) is already in
Datomic. See the `(comment ...)` block at the bottom for the API-based
fallback if you need to repair charges that have no parent sales order.
Dry run by default. Pass `:apply? true` to write."
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.jobs.core :refer [execute]]
[auto-ap.jobs.sales-summaries :as summaries]
[auto-ap.logging :as alog]
[auto-ap.square.core3 :as square3]
[auto-ap.time :as atime]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clj-time.format :as f]
[clj-time.periodic :as per]
[clojure.string :as str]
[config.core :refer [env]]
[datomic.api :as dc]
;; the datalog below calls iol-ion.query/scan-charges by fully-qualified
;; symbol, so the namespace has to be loaded.
[iol-ion.query]))
;; ---------------------------------------------------------------------------
;; Date handling
;; ---------------------------------------------------------------------------
(def pacific (time/time-zone-for-id "America/Los_Angeles"))
(def default-start-date
"The Olo fix landed 2026-07-22; the user-visible bad data starts 7/7."
"2026-07-07")
(defn parse-day
"\"2026-07-07\" -> local (Pacific) midnight DateTime. Passes DateTimes through."
[d]
(cond
(nil? d) nil
(string? d) (f/parse (f/with-zone (f/formatter "yyyy-MM-dd") pacific) d)
:else (coerce/to-date-time d)))
(defn local-midnight [dt]
(.toDateMidnight (atime/localize dt)))
;; ---------------------------------------------------------------------------
;; Classification -- delegates to the production importer
;; ---------------------------------------------------------------------------
(defn recompute-processor
"Runs the *live* classifier over a charge's stored inputs.
`tender->charge` only reads `(:name (:source order))` from the order and
`:type`/`:note` from the tender when deciding `:charge/processor`, so a
synthetic order/tender carrying the stored values yields exactly what a
fresh import would produce today. `:created_at` and `:id` are placeholders
purely to keep the unrelated `:charge/date` and `:charge/reference-link`
branches from blowing up; we only read `:charge/processor` back out.
Note on nil sources: the importer stores `(or (:name (:source order))
\"Square\")`, so a source that was originally nil comes back as \"Square\".
Both nil and \"Square\" classify to `:ccp-processor/na`, so the round trip
is faithful."
[{:keys [source note type-name]}]
(:charge/processor
(square3/tender->charge {:source {:name source}
:created_at "1970-01-01T00:00:00Z"}
{} ; client -> only :charge/client
{} ; location -> only :charge/location
{:id "backfill"
:type type-name
:note note})))
;; ---------------------------------------------------------------------------
;; Scanning
;; ---------------------------------------------------------------------------
(def charge-pull
[:db/id
:charge/external-id
:charge/type-name
:charge/note
:charge/date
:charge/total
{:charge/processor [:db/ident]}
{:sales-order/_charges [:db/id
:sales-order/external-id
:sales-order/source
{:sales-order/vendor [:db/ident]}]}])
(defn client-charges
"All charges for `client-eid` between `start-inst` and `end-inst` (both
inclusive by day), via the :charge/client+date index."
[db client-eid start-inst end-inst]
(->> (dc/q '[:find (pull ?c pull-pattern)
:in $ pull-pattern [?clients ?start ?end]
:where
[(iol-ion.query/scan-charges $ ?clients ?start ?end) [[?c _ _] ...]]]
db
charge-pull
[[client-eid] start-inst end-inst])
(map first)))
(defn parent-order
"Reverse refs on component attributes come back as a single map from pull,
but tolerate a collection in case that ever changes."
[charge]
(let [o (:sales-order/_charges charge)]
(if (sequential? o) (first o) o)))
(defn square-charge? [charge]
(= :vendor/ccp-square (get-in (parent-order charge) [:sales-order/vendor :db/ident])))
;; ---------------------------------------------------------------------------
;; Planning
;; ---------------------------------------------------------------------------
(defn charge->change
"nil when the charge is already correct (or isn't ours to touch)."
[charge]
(when (square-charge? charge)
(let [order (parent-order charge)
current (get-in charge [:charge/processor :db/ident])
next (recompute-processor {:source (:sales-order/source order)
:note (:charge/note charge)
:type-name (:charge/type-name charge)})]
(when (not= current next)
{:charge (:db/id charge)
:external-id (:charge/external-id charge)
:order (:sales-order/external-id order)
:date (:charge/date charge)
:source (:sales-order/source order)
:note (:charge/note charge)
:type-name (:charge/type-name charge)
:total (:charge/total charge)
:from current
:to next}))))
(defn downgrade?
"A change that *loses* processor information. The Olo fix can only ever widen
matching, so these should not exist -- if they do, something else changed
and we'd rather report than clobber."
[{:keys [from to]}]
(and (= :ccp-processor/na to)
(not= :ccp-processor/na from)
(some? from)))
(defn plan-for-client
[db {:keys [db/id client/code]} start-inst end-inst allow-downgrade?]
(let [charges (client-charges db id start-inst end-inst)
square (filter square-charge? charges)
orphans (->> charges
(remove (comp some? parent-order))
(filter #(some-> (:charge/external-id %)
(str/starts-with? "square/charge/"))))
all (keep charge->change square)
[skipped changes] (if allow-downgrade?
[[] all]
[(filter downgrade? all) (remove downgrade? all)])]
{:client id
:code code
:scanned (count square)
:orphan-charges (count orphans)
:changes (vec changes)
:skipped (vec skipped)}))
(defn build-plan
[db clients start-inst end-inst allow-downgrade?]
(->> clients
(map #(plan-for-client db % start-inst end-inst allow-downgrade?))
(remove #(and (zero? (count (:changes %)))
(zero? (count (:skipped %)))
(zero? (:orphan-charges %))))
vec))
;; ---------------------------------------------------------------------------
;; Reporting
;; ---------------------------------------------------------------------------
(defn- transitions [changes]
(->> changes
(group-by (juxt :from :to))
(map (fn [[[from to] cs]]
[from to (count cs) (reduce + 0.0 (keep :total cs))]))
(sort-by #(- (nth % 2)))))
(defn print-report [plan]
(println)
(println "=== Olo processor backfill ===")
(doseq [{:keys [code scanned changes skipped orphan-charges]} plan]
(println)
(printf "%-8s scanned %d square charges, %d to update%n"
(str code) scanned (count changes))
(doseq [[from to n total] (transitions changes)]
(printf " %-24s -> %-24s %5d $%.2f%n"
(str from) (str to) n total))
(when (seq skipped)
(printf " !! %d downgrade(s) to :ccp-processor/na SKIPPED (pass :allow-downgrade? true to force)%n"
(count skipped))
(doseq [{:keys [external-id order source note from]} (take 10 skipped)]
(printf " %s (order %s, source %s, note %s) was %s%n"
external-id order (pr-str source) (pr-str note) from)))
(when (pos? orphan-charges)
(printf " note: %d square charge(s) have no parent sales order (custom-amount tenders).%n"
orphan-charges)
(println " Their order source is not stored, so they cannot be reclassified")
(println " locally -- use the re-import fallback in the comment block.")))
(let [total (reduce + 0 (map (comp count :changes) plan))]
(println)
(printf "TOTAL: %d charge(s) across %d client(s)%n" total (count plan))
(println)
total))
;; ---------------------------------------------------------------------------
;; Applying
;; ---------------------------------------------------------------------------
(defn apply-plan!
"Transacts the processor corrections in batches. Returns the number written."
[plan]
(let [tx (for [{:keys [changes]} plan
{:keys [charge to]} changes]
{:db/id charge :charge/processor to})]
(doseq [batch (partition-all 100 tx)]
(alog/info ::updating-charges :count (count batch))
@(dc/transact-async conn batch))
(count tx)))
(defn mark-summaries-dirty!
"Processor drives the Card / Food App / Unknown split in
`sales-summaries/get-payment-items`, so every day we touched has to be
recalculated. `periodic-seq` is end-exclusive, hence the +1 day."
[plan start end]
(doseq [{:keys [client code changes]} plan
:when (seq changes)]
(alog/info ::marking-summaries-dirty :client code)
(summaries/mark-dirty client
(local-midnight start)
(time/plus (local-midnight end) (time/days 1)))))
;; ---------------------------------------------------------------------------
;; Entry point
;; ---------------------------------------------------------------------------
(defn backfill!
"Recompute :charge/processor for imported Square charges.
Options:
:start \"2026-07-07\" (default) or a DateTime -- inclusive
:end \"2026-07-29\" or a DateTime -- inclusive, default today
:codes seq of client codes; default every Square client
:apply? false (default) = dry run, print only
:allow-downgrade? false (default) = refuse changes that drop a known
processor back to :ccp-processor/na
:mark-dirty? true (default when applying) = flag sales summaries for
recalculation
Returns the plan so you can inspect individual charges in the REPL."
[& {:keys [start end codes apply? allow-downgrade? mark-dirty?]
:or {start default-start-date
apply? false
allow-downgrade? false}}]
(let [start-dt (parse-day start)
end-dt (or (parse-day end) (atime/localize (time/now)))
start-inst (coerce/to-date (local-midnight start-dt))
end-inst (coerce/to-date (local-midnight end-dt))
db (dc/db conn)
clients (if (seq codes)
(apply square3/get-square-clients codes)
(square3/get-square-clients))
_ (alog/info ::scanning
:start start-inst
:end end-inst
:clients (count clients)
:dry-run? (not apply?))
plan (build-plan db clients start-inst end-inst allow-downgrade?)
total (print-report plan)]
(if-not apply?
(do (println "DRY RUN -- nothing written. Re-run with :apply? true to commit.")
(alog/info ::dry-run-complete :change-count total))
(do
(alog/info ::applying :change-count total)
(apply-plan! plan)
(when (not= false mark-dirty?)
(mark-summaries-dirty! plan start-dt end-dt)
(println "Sales summaries marked dirty. Run auto-ap.jobs.sales-summaries")
(println "(or `(auto-ap.jobs.sales-summaries/sales-summaries-v2)`) to rebuild them."))
(alog/info ::done :change-count total)))
plan))
(defn -main [& _]
(execute "backfill-olo-processors"
(fn []
(let [{:keys [start end codes apply allow-downgrade mark-dirty]} (:args env)]
(backfill! :start (or start default-start-date)
:end end
:codes (cond-> codes (string? codes)
(str/split #","))
:apply? (boolean apply)
:allow-downgrade? (boolean allow-downgrade)
:mark-dirty? (if (nil? mark-dirty) true (boolean mark-dirty)))))))
(comment
;; ---------------------------------------------------------------------
;; 1. Dry run everything from 7/7 -- read only, prints what would change.
;; ---------------------------------------------------------------------
(def plan (backfill!))
;; One client at a time while you sanity check.
(backfill! :codes ["NGCL"])
;; Eyeball the actual charges behind a transition.
(->> plan
(mapcat :changes)
(filter #(= :ccp-processor/doordash (:to %)))
(map (juxt :date :source :note :total :from :to))
(take 20))
;; Every distinct source string we saw reclassified -- confirms the Olo
;; variants ("Olo - DoorDash", "olo-ubereats", ...) are what moved.
(->> plan (mapcat :changes) (map :source) frequencies)
;; ---------------------------------------------------------------------
;; 2. Commit, then rebuild the summaries.
;; ---------------------------------------------------------------------
(backfill! :apply? true)
(auto-ap.jobs.sales-summaries/sales-summaries-v2)
;; ---------------------------------------------------------------------
;; 3. Verify: no square charge in the window disagrees with the classifier.
;; ---------------------------------------------------------------------
(->> (backfill!) (mapcat :changes) count) ; => 0
;; ---------------------------------------------------------------------
;; Fallback: charges with no parent sales order (custom-amount tenders,
;; see `is-order-only-for-charge?` in square3/order->sales-order) do not
;; store the order source, so they cannot be fixed locally. Re-import them
;; through the real pipeline instead -- `upsert` is idempotent on
;; :charge/external-id / :sales-order/external-id, so re-running a day is
;; safe and rewrites the processor from live Square data.
;;
;; This hits the Square API for every day x location; the existing
;; auto-ap.jobs.load-historical-sales job does the same thing by day count.
;; ---------------------------------------------------------------------
(doseq [client (square3/get-square-clients)
location (:client/square-locations client)
:when (:square-location/client-location location)
d (per/periodic-seq (parse-day "2026-07-07")
(time/plus (atime/localize (time/now)) (time/days 1))
(time/days 1))]
(println (:client/code client) (:square-location/client-location location) (str d))
@(square3/upsert client location d (time/plus d (time/days 1))))
;; ...then mark dirty + rebuild summaries as in step 2.
)

View File

@@ -0,0 +1,314 @@
(ns auto-ap.jobs.rekey-square-external-ids
"One-shot migration re-keying Square entities to client-scoped external ids.
Refunds, charges, Square payouts (expected deposits) and cash drawer shifts all carry keys with
no client scoping, so two clients configured on the same Square location share a single entity:
its owner flips every time either client imports. Sales orders and ezCater orders already scope
their keys by client and location; this brings the rest in line.
Measured on a restored production backup, ownership had actually changed on 3,387 refunds,
4,069 expected deposits and 2,628 cash drawer shifts, across 19 distinct client pairs — nine of
which no longer share a location in the current configuration and so are invisible to any
point-in-time check.
Run AFTER the importer knows how to resolve both key schemes (`square.core3/existing-id`).
Running it first would be harmless, but the importer would then re-create legacy-keyed
entities on its next pass.
The migration is idempotent: an entity already carrying its scoped key is skipped, so it can
be re-run over a partially migrated database."
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.logging :as alog]
[datomic.api :as dc]))
(def refund-prefix "square/refund/")
(def charge-prefix "square/charge/")
(def deposit-prefix "square/payout/")
(def shift-prefix "square/cash-drawer-shift/")
(defn- scope-of
"`[client-code location]` for an entity, or nil when it cannot be determined.
Charges are the awkward case: about an eighth of them carry neither `:charge/client` nor
`:charge/location`. Those are stubs minted by the payout path, which asserts an external id
alone and lets unique-identity upsert bring a bare entity into being, plus older tender
records that predate the client attribute. None are orphaned, so the scope is recovered from
whatever references them — the sales order first, then the expected deposit."
[db attr e]
(let [ent (dc/entity db e)
pair (fn [code loc] (when (and code loc) [code loc]))]
(or (case attr
:sales-refund/external-id (pair (:client/code (:sales-refund/client ent))
(:sales-refund/location ent))
:charge/external-id (pair (:client/code (:charge/client ent))
(:charge/location ent))
:expected-deposit/external-id (pair (:client/code (:expected-deposit/client ent))
(:expected-deposit/location ent))
:cash-drawer-shift/external-id (pair (:client/code (:cash-drawer-shift/client ent))
(:cash-drawer-shift/location ent)))
(when-let [o (:e (first (dc/datoms db :vaet e :sales-order/charges)))]
(let [oe (dc/entity db o)]
(pair (:client/code (:sales-order/client oe)) (:sales-order/location oe))))
(when-let [d (:e (first (dc/datoms db :vaet e :expected-deposit/charges)))]
(let [de (dc/entity db d)]
(pair (:client/code (:expected-deposit/client de)) (:expected-deposit/location de)))))))
(defn planned-key
"`[eid new-key]` for an entity that still needs re-keying, or nil when it is already scoped or
cannot be scoped at all.
Detection compares against the key this entity *should* have rather than pattern-matching the
id, because Square ids may themselves contain dashes and no pattern separates the two schemes
reliably. That also makes the migration idempotent."
[db attr prefix datom]
(let [old (:v datom)]
(when-let [[code loc] (scope-of db attr (:e datom))]
(let [scoped-prefix (str prefix code "-" loc "-")]
(when-not (.startsWith ^String old scoped-prefix)
[(:e datom) (str scoped-prefix (subs old (count prefix)))])))))
(defn plan
"Everything the migration would change, plus what it cannot touch. Read-only — run this and
check `:collisions` is empty before transacting anything."
[db attr prefix]
(let [acc (reduce (fn [acc d]
(let [acc (update acc :total inc)]
(if-let [[e new-key] (planned-key db attr prefix d)]
(-> acc
(update :to-migrate inc)
(update :new-keys conj! [e new-key]))
(if (scope-of db attr (:e d))
(update acc :already-scoped inc)
(update acc :unscopable inc)))))
{:total 0 :to-migrate 0 :already-scoped 0 :unscopable 0 :new-keys (transient [])}
(dc/datoms db :aevt attr))]
(update acc :new-keys persistent!)))
(defn collisions
"Any two entities that would land on the same new key. Must be empty: a collision would merge
two entities into one and lose whichever lost."
[new-keys]
(->> new-keys
(group-by second)
(keep (fn [[k es]] (when (> (count es) 1) [k (mapv first es)])))
vec))
(defn migrate!
"Asserts the new external id on each planned entity. The attribute is cardinality one, so the
legacy value is retracted by the same assertion and the entity keeps its identity — nothing is
created and nothing is deleted.
Returns the number of entities re-keyed."
[attr new-keys batch-size]
(let [total (count new-keys)]
(alog/info ::migrating :attr attr :count total)
(doseq [[i batch] (map-indexed vector (partition-all batch-size new-keys))]
@(dc/transact conn (for [[e new-key] batch] {:db/id e attr new-key}))
(when (zero? (mod i 20))
(alog/info ::migrated :attr attr :done (* i batch-size) :of total)))
total))
(def charge-copy-attrs
"Everything a charge carries in its own right. `:charge/client+date` is a tuple Datomic
maintains, and `:charge/external-id` is set separately, so neither is copied."
[:charge/type-name :charge/total :charge/tip :charge/tax :charge/date
:charge/processor :charge/note :charge/reference-link])
(defn- raw-square-id
"The Square id inside a charge's external id, with any client scoping removed.
Stripping only the `square/charge/` prefix is not enough. Once a charge has been scoped to one
client, a second order processing the same charge would read `NGCC-CC-<id>` as the id and scope
it again, producing `square/charge/NGCD-CD-NGCC-CC-<id>`. The importer then computes the
correct single-scoped key, fails to find it, and creates a second charge — silently doubling
the tender.
Client codes may themselves contain dashes, so the scope cannot be recognised by pattern. It is
recovered from the entity instead: whoever the charge currently belongs to is exactly whose
scope its key carries."
[db charge old]
(let [ent (dc/entity db charge)
code (:client/code (:charge/client ent))
loc (:charge/location ent)
owner-prefix (when (and code loc) (str charge-prefix code "-" loc "-"))]
(cond
(and owner-prefix (.startsWith ^String old ^String owner-prefix)) (subs old (count owner-prefix))
(.startsWith ^String old charge-prefix) (subs old (count charge-prefix))
:else old)))
(defn- charge-plan-for-order
"What one order needs doing to its charges.
`:keep` — no other order has claimed this charge yet, so this order takes it and it is renamed
in place. `:clone` — another order already owns it, so this order needs its own copy.
Splitting is per client, not per order. Where two orders of the SAME client and location refer
to one payment — Square splitting a tender across orders, or an amendment — they are left
sharing it deliberately. Both would compute the same name, so there is no second name to give
a copy, and more importantly a copy would double that client's takings for the day. The
component cascade still applies to those, which is why the retraction guard on
`remove-voided-orders` is needed regardless of this migration.
Whether a charge is claimed is read from the charge itself: an unclaimed one still carries the
bare `square/charge/<id>` form. Deciding it that way rather than by remembering
every charge seen so far is what lets this run across all 16 million of them — the alternative
needs a map of the entire table in memory. `batch-seen` covers only the orders inside one
transaction, where the database snapshot cannot yet show a claim made moments earlier."
[db order-eid batch-seen]
(let [oe (dc/entity db order-eid)
code (:client/code (:sales-order/client oe))
loc (:sales-order/location oe)]
(when (and code loc)
(for [d (dc/datoms db :eavt order-eid :sales-order/charges)
:let [charge (:v d)
old (:v (first (dc/datoms db :eavt charge :charge/external-id)))]
:when old
:let [raw (raw-square-id db charge old)
new-key (str charge-prefix code "-" loc "-" raw)
claimed (get @batch-seen charge)
unclaimed? (and (= old (str charge-prefix raw)) (nil? claimed))]
;; nothing to do when the charge already answers to this order's name, or when
;; another order in this same batch has just claimed it under that very name —
;; that is the same-client case, which stays shared
:when (and (not= old new-key) (not= claimed new-key))]
{:order order-eid :charge charge :new-key new-key :raw raw
:client (:db/id (:sales-order/client oe)) :location loc
:action (if unclaimed? :keep :clone)}))))
(defn split-and-rekey-charges!
"Gives every order its own charge entity, keyed by that order's client and location.
Where two clients were configured on one Square location, both clients' orders resolved to a
single charge, because charge keys carried no client. Re-keying alone does not undo that — it
hands the one entity to whichever client is looked at first and leaves the other order pointing
at a charge it does not own. Since `:sales-order/charges` is a component attribute, that is not
merely untidy: retracting either order would delete a charge the other one still needs.
So a shared charge is cloned. The first order to claim it keeps it, re-keyed to that order's
scope; every other order gets a copy carrying the same amounts, scoped to its own client, and
has its reference repointed. Afterwards no charge has more than one parent order and the
component relationship means what it says.
`orders` is the collection of order entity ids to process — typically every order belonging to
the clients that share, or have ever shared, a Square location."
[orders batch-size]
(let [cloned (atom 0)
rekeyed (atom 0)]
(doseq [batch (partition-all batch-size orders)]
(let [db (dc/db conn)
batch-seen (atom {})
tx (doall
(for [o batch
plan (charge-plan-for-order db o batch-seen)
:let [{:keys [charge new-key action client location]} plan]
tx-item (if (= :keep action)
(do (swap! batch-seen assoc charge new-key)
(swap! rekeyed inc)
;; record who claimed it: the owner is how a later order
;; recovers the Square id from an already-scoped key
[{:db/id charge
:charge/external-id new-key
:charge/client client
:charge/location location}])
(let [ent (dc/entity db charge)
copy (reduce (fn [m a] (if-some [v (get ent a)]
(assoc m a (if (map? v) (:db/id v) v))
m))
{} charge-copy-attrs)]
(swap! cloned inc)
[(assoc copy
:db/id new-key
:charge/external-id new-key
:charge/client client
:charge/location location)
[:db/retract o :sales-order/charges charge]
{:db/id o :sales-order/charges new-key}]))]
tx-item))]
(when (seq tx) @(dc/transact conn tx))))
(alog/info ::split-charges :rekeyed @rekeyed :cloned @cloned)
{:rekeyed @rekeyed :cloned @cloned}))
(defn charges-with-multiple-parents
"The §3.3 gate. Must read zero once the split has run: while any charge has two parent orders,
retracting either order deletes the other one's payment."
[db orders]
(->> orders
(mapcat (fn [o] (map :v (dc/datoms db :eavt o :sales-order/charges))))
distinct
(filter (fn [c] (> (reduce (fn [n _] (inc n)) 0 (dc/datoms db :vaet c :sales-order/charges)) 1)))
count))
(def scoped-attrs
"Every Square-imported entity whose key must carry its client, with the prefix and where to
read the owner from."
[{:attr :sales-refund/external-id :prefix refund-prefix
:client :sales-refund/client :location :sales-refund/location}
{:attr :charge/external-id :prefix charge-prefix
:client :charge/client :location :charge/location}
{:attr :expected-deposit/external-id :prefix deposit-prefix
:client :expected-deposit/client :location :expected-deposit/location}
{:attr :cash-drawer-shift/external-id :prefix shift-prefix
:client :cash-drawer-shift/client :location :cash-drawer-shift/location}])
(defn unscoped-report
"Counts, per entity type, how many keys are already client-scoped, how many still carry the
legacy unscoped form, and how many have no owner to scope by.
This is the completeness gate. The importer tolerates both key schemes on purpose, so that the
change can be deployed before the migration finishes — but that tolerance is a transition, not
a resting place. While `:legacy` is above zero the database is in a mixed state and a stray
unscoped record can still be adopted by whichever client imports it first. Once every count
reads zero the fallback lookup in `square.core3/existing-id` can be removed and the guarantee
becomes structural rather than conventional."
[db]
(into {}
(for [{:keys [attr prefix client location]} scoped-attrs]
[attr (reduce (fn [acc d]
(let [e (dc/entity db (:e d))
code (:client/code (client e))
loc (location e)
scoped (when (and code loc) (str prefix code "-" loc "-"))]
(cond
(and scoped (.startsWith ^String (:v d) ^String scoped)) (update acc :scoped inc)
(nil? scoped) (update acc :no-owner inc)
:else (update acc :legacy inc))))
{:scoped 0 :legacy 0 :no-owner 0}
(dc/datoms db :aevt attr))])))
(defn all-order-ids
"Every sales order in the database, streamed."
[db]
(map :e (dc/datoms db :aevt :sales-order/external-id)))
(defn migrate-all!
"The complete migration, over the whole database rather than a chosen subset.
Splitting is driven from orders, because a payment's rightful owner is whichever order refers
to it — so every order has to be walked, not merely the clients that share a location today.
Nine client pairs contended in the past and no longer share one; their records are still mixed,
and a migration scoped to the current configuration would miss every one of them.
Returns the split counts and the completeness report, which should read zero legacy across the
board when this finishes."
[batch-size]
(let [split (split-and-rekey-charges! (all-order-ids (dc/db conn)) batch-size)]
(doseq [{:keys [attr prefix]} scoped-attrs
:when (not= attr :charge/external-id)]
(let [p (plan (dc/db conn) attr prefix)]
(when-let [c (seq (collisions (:new-keys p)))]
(throw (ex-info "two entities would take the same key" {:attr attr :collisions (count c)})))
(migrate! attr (:new-keys p) batch-size)))
;; charges no order refers to — payout stubs — are scoped from the deposit that holds them
(let [p (plan (dc/db conn) :charge/external-id charge-prefix)]
(when (seq (:new-keys p)) (migrate! :charge/external-id (:new-keys p) batch-size)))
{:split split :completeness (unscoped-report (dc/db conn))}))
(defn counts
"Entity totals, for the before/after assertion that is this migration's real safety net: if
either number moves, the re-key created duplicates instead of updating in place."
[db]
{:refunds (reduce (fn [n _] (inc n)) 0 (dc/datoms db :aevt :sales-refund/external-id))
:charges (reduce (fn [n _] (inc n)) 0 (dc/datoms db :aevt :charge/external-id))
:deposits (reduce (fn [n _] (inc n)) 0 (dc/datoms db :aevt :expected-deposit/external-id))
:shifts (reduce (fn [n _] (inc n)) 0 (dc/datoms db :aevt :cash-drawer-shift/external-id))})

View File

@@ -1,5 +1,6 @@
(ns auto-ap.jobs.sales-summaries
(:require [auto-ap.datomic :refer [conn]]
[auto-ap.datomic.sales-summaries :as d-ss]
[auto-ap.jobs.core :refer [execute]]
[auto-ap.logging :as alog]
[auto-ap.time :as atime]
@@ -8,6 +9,7 @@
[clj-time.periodic :as per]
[clojure.string :as str]
[com.brunobonacci.mulog :as mu]
[config.core :refer [env]]
[datomic.api :as dc]))
(defn mark-dirty [client start end]
@@ -39,26 +41,108 @@
(dc/db conn)
number)))
(defn delete-all []
@(dc/transact-async conn
(->>
(dc/q '[:find ?ss
:where [?ss :sales-summary/date]]
(dc/db conn))
(map (fn [[ ss]]
[:db/retractEntity ss])))))
(->>
(dc/q '[:find ?ss
:where [?ss :sales-summary/date]]
(dc/db conn))
(map (fn [[ss]]
[:db/retractEntity ss])))))
(def item-read
"Enough of a summary item to both evaluate `d-ss/accepted?` and transact the item back
unchanged. `:db/id` matters: `:sales-summary/items` is a component attribute upserted via
`[:reset-rels ...]`, so an item re-transacted without its id is deleted and recreated."
'[:db/id
:sales-summary-item/category
:sales-summary-item/sort-order
:sales-summary-item/manual?
:ledger-mapped/amount
{:ledger-mapped/ledger-side [:db/ident]}
{:ledger-mapped/account [:db/id]}])
(defn dirty-sales-summaries
"The client's dirty summaries, with enough of each item to evaluate and re-transact it.
(defn dirty-sales-summaries [c]
`index-pull` returns a lazy seq running from `:start` to the END of the index, so this must
stop at the client boundary rather than filter: `:sales-summary/client+dirty` sorts by client
first, so every later client's summaries sit beyond this client's and filtering would walk all
of them — for every client — pulling their items on the way. That is quadratic in the number of
summaries, and it showed up as a full refresh degrading from ~180 client-days a minute to ~3 as
the summary count grew."
[c]
(let [client-id (dc/entid (dc/db conn) c)]
(->> (dc/index-pull (dc/db conn)
{:index :avet
:selector '[:sales-summary/date :sales-summary/client :db/id]
:selector (conj '[:sales-summary/date :sales-summary/client :db/id]
{:sales-summary/items item-read})
:start [:sales-summary/client+dirty [client-id true]]})
(filter (fn [sales-summary]
(= client-id (:db/id (:sales-summary/client sales-summary))))))))
(take-while (fn [sales-summary]
(= client-id (:db/id (:sales-summary/client sales-summary))))))))
(def default-days
"How far back the scheduled refresh looks for summaries that still need recomputing."
7)
(defn trailing-window
"`[start end)` covering the last `days` business days, ending with today. `end` is
exclusive, matching both `periodic-seq`'s 3-arity and the grid's date filters, so
`(trailing-window 7)` is day -6 through today inclusive."
[days]
[(.toDateMidnight (atime/localize (time/minus (time/now) (time/days (dec days)))))
(.toDateMidnight (atime/localize (time/plus (time/now) (time/days 1))))])
(defn accepted-client+dates
"Set of `[client-id date]` pairs in `[start end)` whose summary is already accepted, and
so should be left alone rather than re-marked. Accepted means balanced with every line
mapped to an account — the condition the grid renders as \"Balanced\"."
[db start end]
(->> (dc/q '[:find (pull ?ss selector)
:in $ ?start ?end selector
:where
[?ss :sales-summary/date ?d]
[(>= ?d ?start)]
[(< ?d ?end)]]
db
(c/to-date start)
(c/to-date end)
(conj '[:sales-summary/date {:sales-summary/client [:db/id]}]
{:sales-summary/items item-read}))
(map first)
(filter #(d-ss/accepted? (map d-ss/<-pulled-item (:sales-summary/items %))))
(map (juxt (comp :db/id :sales-summary/client) :sales-summary/date))
set))
(defn mark-stale-dirty
"Marks every client/day in the trailing `days` window dirty so `sales-summaries-v2` will
recompute it, skipping days whose summary is already accepted. Because accepted is derived
rather than stored, a summary that later falls out of balance is picked up again on the
next run. Returns the number of client/days marked."
[days]
(let [db (dc/db conn)
[start end] (trailing-window days)
accepted (accepted-client+dates db start end)
clients (map first (dc/q '[:find ?c
:in $
:where [_ :sales-order/client ?c]]
db))
dates (map c/to-date (per/periodic-seq start end (time/days 1)))
tx-data (for [client clients
date dates
:when (not (accepted [client date]))]
{:sales-summary/client client
:sales-summary/date date
:sales-summary/dirty true
:sales-summary/client+date [client date]})]
(alog/info ::marking-dirty
:days days
:client-count (count clients)
:accepted-count (count accepted)
:marked (count tx-data))
(doseq [batch (partition-all 500 tx-data)]
@(dc/transact conn batch))
(count tx-data)))
(defn- get-fee [c date]
(- (or (ffirst (dc/q '[:find ?f
@@ -72,8 +156,18 @@
date))
0.0)))
(def service-charges-account
"Where a credited Square service charge lands. 49000 is the existing \"Service Income\"
revenue account, which is the closest fit for auto-gratuity and catering fees.
NEEDS ACCOUNTING SIGN-OFF before `service-charges-flag` is enabled for any client: the wrong
account misstates revenue, and a category with no account at all keeps a day from ever
reaching accepted, since `accepted?` requires every line to be mapped."
49000)
(def name->number
{"gyros and pitas" 40111
"service charges" service-charges-account
"returns" 41300
"card payments" 75460
"cash payments" 75452
@@ -99,53 +193,53 @@
"food app refunds" 41400})
(defn get-payment-items [c date]
(->>
(dc/q '[:find ?processor ?type-name (sum ?total)
:with ?c
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-order/charges ?c]
[?c :charge/type-name ?type-name]
(or-join [?c ?processor]
(and [?c :charge/processor ?p]
[?p :db/ident ?processor])
(and
(not [?c :charge/processor])
[(ground :ccp-processor/na) ?processor]))
[?c :charge/total ?total]]
(dc/db conn)
[[c] date date])
(reduce
(fn [acc [processor type-name total]]
(update
acc
(cond (= type-name "CARD")
"Card Payments"
(= type-name "CASH")
"Cash Payments"
(#{"SQUARE_GIFT_CARD" "WALLET" "GIFT_CARD"} type-name)
"Gift Card Payments"
(#{:ccp-processor/toast
#_:ccp-processor/ezcater
#_:ccp-processor/koala
:ccp-processor/doordash
:ccp-processor/grubhub
:ccp-processor/uber-eats} processor)
"Food App Payments"
:else
"Unknown")
(fnil + 0.0)
total))
{})
(map (fn [[k v]]
{:db/id (str (java.util.UUID/randomUUID))
:sales-summary-item/sort-order 0
:sales-summary-item/category k
:ledger-mapped/amount (if (= "Card Payments" k)
(- v (get-fee c date))
v)
:ledger-mapped/ledger-side :ledger-side/debit}))))
(->>
(dc/q '[:find ?processor ?type-name (sum ?total)
:with ?c
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-order/charges ?c]
[?c :charge/type-name ?type-name]
(or-join [?c ?processor]
(and [?c :charge/processor ?p]
[?p :db/ident ?processor])
(and
(not [?c :charge/processor])
[(ground :ccp-processor/na) ?processor]))
[?c :charge/total ?total]]
(dc/db conn)
[[c] date date])
(reduce
(fn [acc [processor type-name total]]
(update
acc
(cond (= type-name "CARD")
"Card Payments"
(= type-name "CASH")
"Cash Payments"
(#{"SQUARE_GIFT_CARD" "WALLET" "GIFT_CARD"} type-name)
"Gift Card Payments"
(#{:ccp-processor/toast
#_:ccp-processor/ezcater
#_:ccp-processor/koala
:ccp-processor/doordash
:ccp-processor/grubhub
:ccp-processor/uber-eats} processor)
"Food App Payments"
:else
"Unknown")
(fnil + 0.0)
total))
{})
(map (fn [[k v]]
{:db/id (str (java.util.UUID/randomUUID))
:sales-summary-item/sort-order 0
:sales-summary-item/category k
:ledger-mapped/amount (if (= "Card Payments" k)
(- v (get-fee c date))
v)
:ledger-mapped/ledger-side :ledger-side/debit}))))
(defn get-discounts [c date]
(when-let [discount (ffirst (dc/q '[:find (sum ?discount)
@@ -162,7 +256,7 @@
:ledger-mapped/ledger-side :ledger-side/debit}))
(defn get-refund-items [c date]
(->>
(->>
(dc/q '[:find ?type-name (sum ?t)
:with ?e
:in $ [?clients ?start-date ?end-date]
@@ -173,26 +267,24 @@
(dc/db conn)
[[c] date date])
(reduce
(fn [acc [type-name total]]
(update
acc
(cond (= type-name "CARD")
"Card Refunds"
(= type-name "CASH")
"Cash Refunds"
:else
"Food App Refunds")
(fnil + 0.0)
total))
{})
(fn [acc [type-name total]]
(update
acc
(cond (= type-name "CARD")
"Card Refunds"
(= type-name "CASH")
"Cash Refunds"
:else
"Food App Refunds")
(fnil + 0.0)
total))
{})
(map (fn [[k v]]
{:db/id (str (java.util.UUID/randomUUID))
:sales-summary-item/sort-order 3
:sales-summary-item/category k
:ledger-mapped/amount v
:ledger-mapped/ledger-side :ledger-side/credit}))))
{:db/id (str (java.util.UUID/randomUUID))
:sales-summary-item/sort-order 3
:sales-summary-item/category k
:ledger-mapped/amount v
:ledger-mapped/ledger-side :ledger-side/credit}))))
(defn get-fees [c date]
(when-let [fee (get-fee c date)]
@@ -219,21 +311,48 @@
[[c] date date]))
0.0)})
(defn- get-tip [c date]
(defn- tendered-tip
"Tips read off the tenders, which is where a tip actually settles."
[c date]
(or (ffirst (dc/q '[:find (sum ?tip)
:with ?c
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-order/charges ?c]
[?c :charge/tip ?tip]]
(dc/db conn)
[[c] date date]))
0.0))
(defn- untendered-tip
"Tips on orders that carry no tender at all. A return-only order reverses its tip on
`:sales-order/tip` but has no charge to join through, so the reversal is invisible to
`tendered-tip` and the day ends up crediting a tip that was handed back."
[c date]
(or (ffirst (dc/q '[:find (sum ?tip)
:with ?e
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-order/tip ?tip]
(not [?e :sales-order/charges])]
(dc/db conn)
[[c] date date]))
0.0))
(defn- get-tip
"Tendered tips plus the tips on untendered orders. Additive rather than substitutive on
purpose: where an order does have a tender, the tender is the correct source, and real
orders exist whose tender carries a tip their `:sales-order/tip` does not — auto-gratuity
booked as a service charge, and wallet tips absent from the net amounts. Reading the order
instead of the tender would drop those."
[c date]
{:ledger-mapped/ledger-side :ledger-side/credit
:sales-summary-item/sort-order 2
:db/id (str (java.util.UUID/randomUUID))
:sales-summary-item/category "Tip"
:ledger-mapped/amount (or (ffirst (dc/q '[:find (sum ?tip)
:with ?c
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-order/charges ?c]
[?c :charge/tip ?tip]]
(dc/db conn)
[[c] date date]))
0.0)})
:ledger-mapped/amount (+ (tendered-tip c date)
(untendered-tip c date))})
(defn- get-sales [c date]
(let [sales (->> (dc/q '[:find ?category (sum ?total) (sum ?tax) (sum ?discount)
@@ -259,36 +378,165 @@
:ledger-mapped/amount (- (+ total discount) tax)
#_#_:ledger-mapped/account nil})))
(defn- get-returns [c date]
(when-let [amount (ffirst (dc/q '[:find (sum ?r)
:with ?e
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-order/returns ?r]
(def refund-only-returns-flag
"Per-client rollout lever for recognising a return on a day that carries refunds but no sales,
in the same style as `service-charges-flag`. Absent, the summary behaves exactly as it does
today."
"summary-refund-only-returns")
#_[?e :sales-order/charges ?c]
#_[?c :charge/tax ?tax]]
(dc/db conn)
[[c] date date]))]
(defn- refund-only-returns-enabled? [c]
(contains? (set (:client/feature-flags (dc/pull (dc/db conn) '[:client/feature-flags] c)))
refund-only-returns-flag))
(defn- traded?
"Whether the client recorded any sales order at all on `date`."
[c date]
(some? (ffirst (dc/q '[:find (count ?e)
:in $ [?clients ?start-date ?end-date]
:where
[(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]
(dc/db conn)
[[c] date date]))))
(defn refunded-total
"Face value of every refund recorded for the client on `date`.
This is the same population the `Card Refunds` / `Cash Refunds` / `Food App Refunds` credits are
built from, read back as one figure so a return can be posted against it."
[c date]
(or (ffirst (dc/q '[:find (sum ?t)
:with ?e
:in $ [?clients ?start-date ?end-date]
:where
[(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-refund/total ?t]]
(dc/db conn)
[[c] date date]))
0.0))
(defn- refund-only-returns
"The return owed by a day that recorded refunds and no sales at all.
Refunds are credited on the day the money goes back; the return that offsets them is read from
`:sales-order/returns` on orders scanned for that same day. When the two fall on the same day
they cancel. When they do not — a refund settling after the restaurant closed for the day, or a
retired duplicate client record that still receives refunds while its orders belong to the
surviving record — the credit has nothing to offset it and the day cannot balance, by exactly
the refunded amount.
Only days with *no* sales orders are touched. That guard is what makes this safe rather than a
judgement call: with no orders there is no order-derived return to double-count, and no trading
day can have its figures moved. Days that do trade and still carry an unmatched refund are left
alone deliberately — apportioning a return across a day that also sold is an accounting
question, not a mechanical one."
[c date]
(when (and (refund-only-returns-enabled? c)
(not (traded? c date)))
(let [amount (refunded-total c date)]
(when-not (zero? amount) amount))))
(defn- get-returns [c date]
(when-let [amount (or (ffirst (dc/q '[:find (sum ?r)
:with ?e
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-order/returns ?r]
#_[?e :sales-order/charges ?c]
#_[?c :charge/tax ?tax]]
(dc/db conn)
[[c] date date]))
(refund-only-returns c date))]
{:db/id (str (java.util.UUID/randomUUID))
:sales-summary-item/category "Returns"
:ledger-mapped/amount amount
:ledger-mapped/ledger-side :ledger-side/debit}))
(defn sales-summaries-v2 []
(doseq [[c client-code] (dc/q '[:find ?c ?client-code
:in $
:where [?c :client/code ?client-code]]
(dc/db conn))
{:sales-summary/keys [date] :db/keys [id] :as existing-summary} (dirty-sales-summaries c)]
(def service-charges-flag
"Per-client rollout lever for crediting Square service charges, in the same style as
`new-square` and `import-custom-amount`. Absent, the summary behaves exactly as it does
today."
"summary-service-charges")
(defn- service-charges-enabled? [c]
(contains? (set (:client/feature-flags (dc/pull (dc/db conn) '[:client/feature-flags] c)))
service-charges-flag))
(defn service-charge-total
"Square service charges for the day, both signs.
A service charge is collected inside the card tender but nothing credits it, so every order
carrying one leaves the day short by exactly that amount. Both signs matter: a returned
catering fee arrives as a negative service charge and is subtracted back out of
`:sales-order/returns`, so dropping negatives would lose the reversal.
The vendor gate is load-bearing — ezCater service charges are commission deducted from the
restaurant rather than collected from the diner, and crediting those would make things worse.
It matches on `:sales-order/vendor` where that is set and falls back to the external id
prefix where it is not, because whole eras of Square orders carry no vendor attribute at all
and a gate on vendor alone silently credits nothing.
Kept separate from the rollout flag so the arithmetic can be measured on its own."
[c date]
(ffirst (dc/q '[:find (sum ?service-charge)
:with ?e
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-order/service-charge ?service-charge]
(or-join [?e]
[?e :sales-order/vendor :vendor/ccp-square]
(and (not [?e :sales-order/vendor])
[?e :sales-order/external-id ?external-id]
[(clojure.string/starts-with? ?external-id "square/order/")]))]
(dc/db conn)
[[c] date date])))
(defn- get-service-charges
"The day's service charges as a summary item, for clients opted in to the rollout."
[c date]
(when (service-charges-enabled? c)
(when-let [amount (service-charge-total c date)]
(when-not (zero? amount)
{:db/id (str (java.util.UUID/randomUUID))
:sales-summary-item/category "Service Charges"
:sales-summary-item/sort-order 2
:ledger-mapped/amount amount
:ledger-mapped/ledger-side :ledger-side/credit}))))
(def ^:private suspect-categories
"The terms a balancing investigation keeps returning to. Logged beside the imbalance so a
day's shape can be read out of the logs without re-running the job."
["Tip" "Service Charges" "Returns" "Card Refunds" "Cash Refunds" "Food App Refunds"])
(defn- suspect-totals
"Amounts for `suspect-categories` present on this day, omitting the ones that are zero."
[items]
(into {}
(for [category suspect-categories
:let [amount (->> items
(filter #(= category (:sales-summary-item/category %)))
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0))]
:when (not (zero? amount))]
[category amount])))
(defn refresh-client!
"Recomputes every dirty summary for one client.
Split out of the driver loop so a client's work stands on its own: it can be run for a single
client, and a backfill over the whole history can spread clients across threads instead of
grinding through the largest ones one day at a time."
[c client-code]
(doseq [{:sales-summary/keys [date] :db/keys [id] :as existing-summary} (dirty-sales-summaries c)]
(mu/with-context {:client-code client-code
:date date}
(alog/info ::updating)
(let [manual-items (->> existing-summary
:sales-summary/items
(filter :sales-summary-item/manual?))
calculated-items (->>
:date date}
(alog/info ::updating)
(let [manual-items (->> existing-summary
:sales-summary/items
(filter :sales-summary-item/manual?)
(map d-ss/<-pulled-item))
calculated-items (->>
(get-sales c date)
(concat (get-payment-items c date))
(concat (get-refund-items c date))
@@ -296,25 +544,37 @@
(cons (get-fees c date))
(cons (get-tax c date))
(cons (get-tip c date))
(cons (get-service-charges c date))
(cons (get-returns c date))
(filter identity)
(map (fn [z]
(assoc z :ledger-mapped/account (some-> z :sales-summary-item/category str/lower-case name->number lookup-account)
:sales-summary-item/manual? false))))
all-items (concat calculated-items manual-items)
result {:db/id id
:sales-summary/client c
:sales-summary/date date
:sales-summary/dirty false
:sales-summary/client+date [c date]
:sales-summary/items all-items}]
(if (seq (:sales-summary/items result))
(do
(alog/info ::upserting-summaries
:category-count (count (:sales-summary/items result)))
@(dc/transact conn [[:upsert-sales-summary result]]))
@(dc/transact conn [{:db/id id :sales-summary/dirty false}]))))))
all-items (concat calculated-items manual-items)
result {:db/id id
:sales-summary/client c
:sales-summary/date date
:sales-summary/dirty false
:sales-summary/client+date [c date]
:sales-summary/items all-items}]
(if (seq (:sales-summary/items result))
(do
(alog/info ::upserting-summaries
:category-count (count (:sales-summary/items result))
:imbalance (d-ss/imbalance all-items)
:balanced? (d-ss/balanced? all-items)
:suspect-totals (suspect-totals all-items))
@(dc/transact conn [[:upsert-sales-summary result]]))
@(dc/transact conn [{:db/id id :sales-summary/dirty false}]))))))
(defn sales-summaries-v2
"Recomputes every dirty summary, client by client."
[]
(doseq [[c client-code] (dc/q '[:find ?c ?client-code
:in $
:where [?c :client/code ?client-code]]
(dc/db conn))]
(refresh-client! c client-code)))
(defn reset-summaries []
@(dc/transact conn (->> (dc/q '[:find ?sos
@@ -324,9 +584,6 @@
(map (fn [[sos]]
[:db/retractEntity sos])))))
(comment
(auto-ap.datomic/transact-schema conn)
@@ -336,26 +593,19 @@
(dirty-sales-summaries [:client/code "NGWH"])
(apply mark-dirty [:client/code "NGWH"] (last-n-days 5))
(iol-ion.tx.upsert-sales-summary-ledger/summary->journal-entry (dc/db conn) 17592314245819)
(iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary (dc/db conn) {:db/id 17592314241429})
(mark-all-dirty 5)
(delete-all)
(sales-summaries-v2)
1
(dc/q '[:find (pull ?sos [* {:sales-summary/sales-items [*]}])
:in $
:where [?sos :sales-summary/client [:client/code "NGHW"]]
@@ -386,15 +636,24 @@
@(dc/transact conn [{:db/id :sales-summary/total-tax :db/ident :sales-summary/total-tax-legacy}
{:db/id :sales-summary/total-tip :db/ident :sales-summary/total-tip-legacy}])
(auto-ap.datomic/transact-schema conn)
)
(auto-ap.datomic/transact-schema conn))
(defn days-arg
"Trailing-window size from the job's `args`, e.g. `{:days 30}` set as a container override
from the admin Background Jobs page for an ad-hoc wider backfill. Values arrive as EDN but
may still be strings, so coerce defensively the way load-historical-sales does."
[args]
(let [days (:days args)]
(cond-> (or days default-days)
(string? days) (#(Long/parseLong %)))))
(defn refresh-sales-summaries
"Marks the trailing `days` window dirty, skipping accepted summaries, then recomputes
everything left dirty."
([] (refresh-sales-summaries default-days))
([days]
(mark-stale-dirty days)
(sales-summaries-v2)))
(defn -main [& _]
(execute "sales-summaries" sales-summaries-v2))
(execute "sales-summaries" #(refresh-sales-summaries (days-arg (:args env)))))

View File

@@ -35,20 +35,35 @@
(into {}))))))
@sysco-name->line)
(defn get-line-account [item-name]
(get (get-sysco->line)
item-name
(ffirst (dc/q '[:find ?a
:in $ ?an
:where [?a :account/numeric-code ?an]]
(dc/db conn)
50000))))
(defn get-account-by-code [numeric-code]
(ffirst (dc/q '[:find ?a
:in $ ?an
:where [?a :account/numeric-code ?an]]
(dc/db conn)
numeric-code)))
;; Sysco categories whose *default* is unambiguous, so a line item whose
;; description is missing from sysco_line_item_mapping.csv still codes
;; correctly instead of silently defaulting to Food Costs. Only PAPER & DISP
;; qualifies: 440 of its 455 mapped descriptions point at 55000, and the 15
;; exceptions (foil pans -> 51500, scour pads -> 74100) are all mapped
;; explicitly, so the description mapping wins ahead of this fallback.
(def category->numeric-code {"PAPER & DISP" 55000})
(def default-numeric-code 50000)
(defn get-line-account
([item-name] (get-line-account item-name nil))
([item-name sysco-category]
(or (get (get-sysco->line) item-name)
(some-> (category->numeric-code sysco-category) get-account-by-code)
(get-account-by-code default-numeric-code))))
(def ^:dynamic bucket-name (:data-bucket env))
(def header-keys ["TransCode" "GroupID" "Company" "CustomerNumber" "InvoiceNumber" "RecordType" "Item" "InvoiceDocument" "AccountName" "AccountDunsNo" "InvoiceDate" "AccountDate" "CustomerPONo" "PaymentTerms" "TermsDescription" "StoreNumber" "CustomerName" "AddressLine1" "AddressLine2" "City1" "State1" "Zip1" "Phone1" "Duns1" "Hin1" "Dea1" "TIDCustomer" "ChainNumber" "BidNumber" "ContractNumber" "CompanyNumber" "BriefName" "Address" "Address2" "City2" "State2" "Zip2" "Phone2" "Duns2" "Hin2" "Dea2" "Tid_OPCO" "ObligationIndicator" "Manifest" "Route" "Stop" "TermsDiscountPercent" "TermsDiscountDueDate" "TermsNetDueDate" "TermsDiscountAmount" "TermsDiscountCode" "OrderDate" "DepartmentCode"])
(def item-price-index 15)
(def item-category-index 25)
(def item-name-index 29)
(def summary-keys ["TranCode" "GroupID" "Company" "CustomerNumber" "InvoiceNumber" "RecordType" "Item" "InvoiceDocument" "TotalLines" "TotalQtyInvoice" "TotalQty" "TotalQtySplit" "TotalQtyPounds" "TotalExtendedPrice" "TotalTaxAmount" "TotalInvoiceAmount" "AccountDate"])
@@ -56,14 +71,13 @@
(defn get-sysco-vendor []
(let [db (dc/db conn)]
(->
(dc/q '[:find (pull ?v r)
:in $ r
:where [?v :vendor/name "Sysco"]]
db
d-vendors/default-read)
first
first)))
(dc/q '[:find (pull ?v r)
:in $ r
:where [?v :vendor/name "Sysco"]]
db
d-vendors/default-read)
first
first)))
(defn read-sysco-csv [k]
(-> (s3/get-object {:bucket-name bucket-name
@@ -73,34 +87,33 @@
csv/read-csv))
(defn check-okay-amount? [i]
(dollars=
(dollars=
(:invoice/total i)
(reduce + 0.0 (map :invoice-expense-account/amount (:invoice/expense-accounts i)))))
(defn code-individual-items [invoice csv-rows tax]
(let [items (->> csv-rows
butlast
(reduce
(fn [acc row]
(update acc (get-line-account (nth row item-name-index))
(fnil + 0.0)
(Double/parseDouble (nth row item-price-index))
)
)
{})
)
(reduce
(fn [acc row]
(update acc (get-line-account (nth row item-name-index)
(nth row item-category-index))
(fnil + 0.0)
(Double/parseDouble (nth row item-price-index))))
{}))
items-with-tax (update items (get-line-account "TAX")
(fnil + 0.0)
(fnil + 0.0)
tax)
updated-invoice (assoc invoice :invoice/expense-accounts
(for [[account amount] items-with-tax]
#:invoice-expense-account {:db/id (random-tempid)
:account account
:location (:invoice/location invoice)
:amount amount}))]
updated-invoice (assoc invoice :invoice/expense-accounts
(for [[account amount] items-with-tax]
#:invoice-expense-account {:db/id (random-tempid)
:account account
:location (:invoice/location invoice)
:amount amount}))]
(if (check-okay-amount? updated-invoice)
updated-invoice
(do (alog/warn ::itemized-expenses-not-adding-up
(do (alog/warn ::itemized-expenses-not-adding-up
:invoice updated-invoice)
invoice))))
@@ -122,11 +135,11 @@
(header-row "AddressLine2")
(header-row "City1")
(header-row "City2")])
account-number (some-> account-number Long/parseLong str)
matching-client (and account-number
(d-clients/exact-match account-number))
_ (when-not matching-client
(throw (ex-info "cannot find matching client"
{:account-number account-number
@@ -153,9 +166,9 @@
:client/locations]
(:db/id matching-client))
location-hint
location-hint )
location-hint)
:date (coerce/to-date date)
:vendor (:db/id sysco-vendor )
:vendor (:db/id sysco-vendor)
:client (:db/id matching-client)
:import-status :import-status/imported
:status :invoice-status/unpaid
@@ -180,64 +193,54 @@
(s3/delete-object {:bucket-name bucket-name
:key k}))
(defn get-test-invoice-file
(defn get-test-invoice-file
([] (get-test-invoice-file 999))
( [i]
([i]
(nth (->> (s3/list-objects-v2 {:bucket-name "data.prod.app.integreatconsult.com"
:prefix "sysco/imported"})
:object-summaries
(map :key)
)
(map :key))
i)))
(comment
(with-bindings { #'bucket-name "data.prod.app.integreatconsult.com"}
(doall
(for [n (range 930 940 )
:let [result (-> (get-test-invoice-file n)
read-sysco-csv
(extract-invoice-details (get-sysco-vendor))
)]
#_#_:when (not (check-okay-amount? result))]
(comment
(with-bindings {#'bucket-name "data.prod.app.integreatconsult.com"}
(doall
(for [n (range 930 940)
:let [result (-> (get-test-invoice-file n)
read-sysco-csv
(extract-invoice-details (get-sysco-vendor)))]
#_#_:when (not (check-okay-amount? result))]
result)))
(with-bindings { #'bucket-name "data.prod.app.integreatconsult.com"}
(let [result (-> "sysco/error/SYSCO050_00175962_20241010122639019.csv"
(with-bindings {#'bucket-name "data.prod.app.integreatconsult.com"}
(let [result (-> "sysco/error/SYSCO050_00175962_20241010122639019.csv"
read-sysco-csv
(extract-invoice-details (get-sysco-vendor))
)]
(extract-invoice-details (get-sysco-vendor)))]
result))
)
result)))
(defn import-sysco []
(let [sysco-vendor (get-sysco-vendor)
keys (->> (s3/list-objects-v2 {:bucket-name bucket-name
:prefix "sysco/pending"})
:object-summaries
(map :key))]
:object-summaries
(map :key))]
(alog/info ::importing-sysco
:count (count keys)
:keys (pr-str keys))
(let [transaction (->> keys
(mapcat (fn [k]
(try
(try
(let [invoice-key (str "invoice-files/" (UUID/randomUUID) ".csv") ;
invoice-url (str "https://" (:data-bucket env) "/" invoice-key)]
(s3/copy-object {:source-bucket-name (:data-bucket env)
:destination-bucket-name (:data-bucket env)
:source-key k
:destination-key invoice-key})
[[:propose-invoice
[[:propose-invoice
(-> k
read-sysco-csv
(extract-invoice-details sysco-vendor)
@@ -246,7 +249,7 @@
(alog/error ::cant-load-file
:file k
:error e e)
(s3/copy-object {:source-bucket-name (:data-bucket env)
(s3/copy-object {:source-bucket-name (:data-bucket env)
:destination-bucket-name (:data-bucket env)
:source-key k
:destination-key (str "sysco/error/"
@@ -256,6 +259,5 @@
(doseq [k keys]
(mark-key k))))
(defn -main [& _]
(execute "sysco" import-sysco))

View File

@@ -742,6 +742,19 @@
:total [:trim-commas-and-negate nil]}
:multi #"\n"
:multi-match? #"^\d+"}
;; REEL PRODUCE STATEMENT (QuickBooks layout -- no "Reel Produce" text on the page)
{:vendor "Reel Produce"
:keywords [#"reelproduce\.com" #"Statement"]
:extract {:date #"^\s*([0-9]+/[0-9]+/[0-9]+)"
:customer-identifier #"To:(?:.*?)\n\s*(.*?)\s{2,}"
:invoice-number #"INV #(\d+)"
:total #"Orig\. Amount \$([\d\-,]+\.\d{2,2})"}
:parser {:date [:clj-time "MM/dd/yyyy"]
:total [:trim-commas-and-negate nil]}
:multi #"\n"
:multi-match? #"^\s*[0-9]+/[0-9]+/[0-9]+\s+INV #"}
{:vendor "Paulino's Bakery"
:keywords [#"paulinosbakery"]
:extract {:date #"\s*([0-9]+/[0-9]+/[0-9]+)"

View File

@@ -13,6 +13,7 @@
[auto-ap.jobs.load-historical-sales :as job-load-historical-sales]
[auto-ap.jobs.plaid :as job-plaid]
[auto-ap.jobs.register-invoice-import :as job-register-invoice-import]
[auto-ap.jobs.sales-summaries :as job-sales-summaries]
[auto-ap.jobs.square :as job-square]
[auto-ap.jobs.sysco :as job-sysco]
[auto-ap.jobs.vendor-usages :as job-vendor-usages]
@@ -33,23 +34,22 @@
(.addShutdownHook (Runtime/getRuntime)
(Thread. f)))
(defn gzip-handler []
(let [gz (GzipHandler.)]
(doto gz
(.setIncludedMethods (into-array ["GET" "POST" "PUT" "DELETE" "PATCH"]))
(.setIncludedMimeTypes (into-array ["text/css"
"text/*"
"text/plain"
"text/javascript"
"text/csv"
"text/html"
"text/html;charset=utf-8"
"application/javascript"
"application/csv"
"application/edn"
"application/json"
"image/svg+xml"]))
(.setIncludedMimeTypes (into-array ["text/css"
"text/*"
"text/plain"
"text/javascript"
"text/csv"
"text/html"
"text/html;charset=utf-8"
"application/javascript"
"application/csv"
"application/edn"
"application/json"
"image/svg+xml"]))
(.setMinGzipSize 1024))
gz))
@@ -126,6 +126,9 @@
(= job "close-auto-invoices")
(job-close-auto-invoices/-main)
(= job "sales-summaries")
(job-sales-summaries/-main)
(= job "ezcater-upsert")
(job-ezcater-upsert/-main)

View File

@@ -269,6 +269,29 @@
0.0
[]))
(defn scoped-key
"Client-scoped external id, in the shape sales order keys already use.
Without the client and location in the key, two clients configured on the same Square location
collide on a single entity: a refund changes owner every time either client imports, and one
charge ends up shared between both clients' orders."
[prefix client location id]
(str prefix (:client/code client) "-" (:square-location/client-location location) "-" id))
(defn existing-id
"Entity id of the refund or charge this id already refers to, trying the client-scoped key
first and the legacy unscoped key second.
This is what makes re-keying safe. These external ids are `:db.unique/identity`, so the import
relies on upsert-by-identity; changing the key format on its own would match nothing and
Datomic would create a SECOND entity for every refund and charge, orphaning the original under
its legacy key. Pinning the result as `:db/id` makes the write land on the existing entity
whichever scheme it currently carries."
[db attr prefix client location id]
(when id
(or (dc/entid db [attr (scoped-key prefix client location id)])
(dc/entid db [attr (str prefix id)]))))
(defn tender->charge [order client location t]
(remove-nils
#:charge
@@ -278,8 +301,9 @@
:note (:note t)
:location (:square-location/client-location location)
:reference-link (str (url/url "https://squareup.com/receipt/preview" (:id t)))
:db/id (existing-id (dc/db conn) :charge/external-id "square/charge/" client location (:id t))
:external-id (when (:id t)
(str "square/charge/" (:id t)))
(scoped-key "square/charge/" client location (:id t)))
:processor (cond
(#{"OTHER" "THIRD_PARTY_CARD"} (:type t))
(condp = (some-> (:note t) str/lower-case)
@@ -290,15 +314,15 @@
"grubhub" :ccp-processor/grubhub
"grub" :ccp-processor/grubhub
"gh" :ccp-processor/grubhub
(condp = (:name (:source order))
"GRUBHUB" :ccp-processor/grubhub
"UBEREATS" :ccp-processor/uber-eats
"Uber Eats" :ccp-processor/uber-eats
"DOORDASH" :ccp-processor/doordash
"DoorDash" :ccp-processor/doordash
"Koala" :ccp-processor/koala
"koala-production" :ccp-processor/koala
:ccp-processor/na))
(let [src (some-> (:name (:source order)) str/lower-case)]
(cond
(nil? src) :ccp-processor/na
(str/includes? src "doordash") :ccp-processor/doordash
(str/includes? src "uber") :ccp-processor/uber-eats
(str/includes? src "grubhub") :ccp-processor/grubhub
(str/includes? src "postmates") :ccp-processor/uber-eats
(str/includes? src "koala") :ccp-processor/koala
:else :ccp-processor/na)))
(= (:type t) "CARD")
:ccp-processor/square
@@ -509,7 +533,7 @@
(try
(->> (for [payout payouts
:let [best-sales-date (some->> (dc/q '[:find ?s4 (count ?s)
:in $ ?payout-id
:in $ [?payout-id ...]
:where
[?payout :expected-deposit/external-id ?payout-id]
[?payout :expected-deposit/charges ?c]
@@ -519,7 +543,8 @@
[(auto-ap.time/localize ?s2) ?s3]
[(clj-time.coerce/to-local-date ?s3) ?s4]]
(dc/db conn)
(str "square/payout/" (:id payout)))
[(scoped-key "square/payout/" client location (:id payout))
(str "square/payout/" (:id payout))])
(sort-by last)
last
first
@@ -543,7 +568,10 @@
(:db/id client)
(amount->money (:amount_money payout))))]
:when (not equivalent-already-exists?)]
#:expected-deposit {:external-id (str "square/payout/" (:id payout))
#:expected-deposit {:db/id (or (existing-id (dc/db conn) :expected-deposit/external-id
"square/payout/" client location (:id payout))
(str "square/payout/" (:id payout)))
:external-id (scoped-key "square/payout/" client location (:id payout))
:vendor :vendor/ccp-square
:status :expected-deposit-status/pending
:total (amount->money (:amount_money payout))
@@ -561,7 +589,11 @@
(coerce/to-date)))
:charges (reverse (->> (:payout_entries payout)
(filter (comp :payment_id :type_charge_details))
(map (fn [p] {:charge/external-id (str "square/charge/" (:payment_id (:type_charge_details p)))}))))})
(map (fn [p]
(let [payment-id (:payment_id (:type_charge_details p))]
(remove-nils
{:charge/external-id (scoped-key "square/charge/" client location payment-id)
:db/id (existing-id (dc/db conn) :charge/external-id "square/charge/" client location payment-id)}))))))})
(filter :expected-deposit/date)
(into []))
(catch Throwable e
@@ -585,7 +617,8 @@
(de/chain
(get-payment client (:payment_id r))
(fn [payment]
#:sales-refund {:external-id (str "square/refund/" (:id r))
#:sales-refund {:db/id (existing-id (dc/db conn) :sales-refund/external-id "square/refund/" client l (:id r))
:external-id (scoped-key "square/refund/" client l (:id r))
:vendor :vendor/ccp-square
:total (amount->money (:amount_money r))
:fee (transduce
@@ -703,7 +736,10 @@
(de/chain
(get-cash-shift client (:id s))
(fn [cash-drawer-shift]
#:cash-drawer-shift {:external-id (str "square/cash-drawer-shift/" (:id cash-drawer-shift))
#:cash-drawer-shift {:db/id (or (existing-id (dc/db conn) :cash-drawer-shift/external-id
"square/cash-drawer-shift/" client l (:id cash-drawer-shift))
(str "square/cash-drawer-shift/" (:id cash-drawer-shift)))
:external-id (scoped-key "square/cash-drawer-shift/" client l (:id cash-drawer-shift))
:vendor :vendor/ccp-square
:paid-in (amount->money (:cash_paid_in_money cash-drawer-shift))
:paid-out (amount->money (:cash_paid_out_money cash-drawer-shift))
@@ -723,10 +759,12 @@
:when (:square-location/client-location square-location)]
(upsert-cash-shifts client square-location))))
([client location]
(upsert-cash-shifts client location (time/plus (time/now) (time/days -75)) (time/now)))
([client location start end]
(with-context-as {:source "Square cash shift loading"
:client (:client/code client)} lc
(de/chain (cash-drawer-shifts client location)
(de/chain (cash-drawer-shifts client location start end)
(fn [cash-shifts]
(mu/with-context lc
(try

View File

@@ -28,14 +28,13 @@
(com.amazonaws.services.ecs.model AssignPublicIp)))
(defn get-ecs-tasks []
(->>
(concat (:task-arns (ecs/list-tasks :max-results 50)) (:task-arns (ecs/list-tasks :desired-status "STOPPED" :max-results 50)))
(ecs/describe-tasks :include [] :tasks)
:tasks
(map #(assoc % :task-definition (:task-definition (ecs/describe-task-definition :task-definition (:task-definition-arn %)))))
(sort-by :created-at)
reverse))
(->>
(concat (:task-arns (ecs/list-tasks :max-results 50)) (:task-arns (ecs/list-tasks :desired-status "STOPPED" :max-results 50)))
(ecs/describe-tasks :include [] :tasks)
:tasks
(map #(assoc % :task-definition (:task-definition (ecs/describe-task-definition :task-definition (:task-definition-arn %)))))
(sort-by :created-at)
reverse))
(defn is-background-job?
"This function checks whether a given task is a background job.
@@ -60,7 +59,7 @@
(defn job-exited-successfully? [task]
(if (= 0 (->> task
:containers
(filter (comp #{"integreat-app" } :name))
(filter (comp #{"integreat-app"} :name))
(first)
:exit-code))
true
@@ -77,7 +76,7 @@
:succeeded
:failed))
:name (task-definition->job-name (:task-definition task))
:end-date (some-> (:stopped-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))
:end-date (some-> (:stopped-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))
:start-date (some-> (:created-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))})
(defn fetch-page [request]
@@ -85,7 +84,7 @@
(filter is-background-job?)
(map ecs-task->job))]
[jobs (count jobs)]))
(def query-schema (mc/schema [:map ]))
(def query-schema (mc/schema [:map]))
(def grid-page
(helper/build {:id "job-table"
@@ -107,8 +106,7 @@
:entity-name "Job"
:query-schema query-schema
:route :admin-job-table
:headers [
{:key "start"
:headers [{:key "start"
:name "Start"
:render #(some-> % :start-date (atime/unparse-local atime/standard-time))}
{:key "end"
@@ -119,7 +117,7 @@
:render (fn [e]
(when (and (:start-date e)
(:end-date e))
(str (time/in-minutes (time/interval
(str (time/in-minutes (time/interval
(:start-date e)
(:end-date e))) " minutes")))}
{:key "name"
@@ -150,16 +148,16 @@
:network-configuration {:aws-vpc-configuration {:subnets ["subnet-5e675761" "subnet-8519fde2" "subnet-89bab8d4"]
:security-groups ["sg-004e5855310c453a3" "sg-02d167406b1082698"]
:assign-public-ip AssignPublicIp/ENABLED}}}
args (assoc-in [:overrides :container-overrides ] [{:name "integreat-app" :environment [{:name "args" :value (pr-str args)}]}]))))
args (assoc-in [:overrides :container-overrides] [{:name "integreat-app" :environment [{:name "args" :value (pr-str args)}]}]))))
(defn job-start [{:keys [form-params]}]
(if (not (get (currently-running-jobs) (:name form-params)))
(let [new-job (run-task
(-> (:name form-params)
(str/replace #"-" "_")
(str/replace #":" "")
(str "_" (:dd-env env)))
(dissoc form-params :name))]
(-> (:name form-params)
(str/replace #"-" "_")
(str/replace #":" "")
(str "_" (:dd-env env)))
(dissoc form-params :name))]
{:message (str "task " (str new-job) " started.")})
(form-validation-error "This job is already running"
:form-params form-params)))
@@ -170,107 +168,109 @@
[(fc/with-field :ledger-url
(com/validated-field {:label "Url"
:errors (fc/field-errors)}
[:div.flex.place-items-center.gap-2
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
(com/text-input {:placeholder "ledger-data.csv"
:name (fc/field-name)
:value (fc/field-value)} )]))]
[:div.flex.place-items-center.gap-2
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
(com/text-input {:placeholder "ledger-data.csv"
:name (fc/field-name)
:value (fc/field-value)})]))]
(= "register-invoice-import" name)
[
(fc/with-field :invoice-url
(com/validated-field {:label "Url"
:errors (fc/field-errors)}
[:div.flex.place-items-center.gap-2
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
(com/text-input {:placeholder "invoice-data.csv"
:name (fc/field-name)
:value (fc/field-value)} )]))]
[(fc/with-field :invoice-url
(com/validated-field {:label "Url"
:errors (fc/field-errors)}
[:div.flex.place-items-center.gap-2
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
(com/text-input {:placeholder "invoice-data.csv"
:name (fc/field-name)
:value (fc/field-value)})]))]
(= "load-historical-sales" name)
[
(fc/with-field :client
(com/validated-field {:label "Client"
:errors (fc/field-errors)}
(com/typeahead {:name (fc/field-name)
:value (fc/field-value)
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes
:company-search)})))
(fc/with-field :days
[(fc/with-field :client
(com/validated-field {:label "Client"
:errors (fc/field-errors)}
(com/typeahead {:name (fc/field-name)
:value (fc/field-value)
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes
:company-search)})))
(fc/with-field :days
(com/validated-field {:label "Days to load"
:errors (fc/field-errors)}
(com/text-input {:placeholder "60"
:name (fc/field-name)
:value (fc/field-value)} )))]
:else nil))
:value (fc/field-value)})))]
(= "sales-summaries" name)
[(fc/with-field :days
(com/validated-field {:label "Days to refresh"
:errors (fc/field-errors)}
(com/text-input {:placeholder "7"
:name (fc/field-name)
:value (fc/field-value)})))]
:else nil)))
)
(defn subform [{{:keys [name]} :query-params }]
(defn subform [{{:keys [name]} :query-params}]
(html-response
(fc/start-form {} nil
(subform* {:name name}))))
(fc/start-form {} nil
(subform* {:name name}))))
(defn job-start-dialog [{:keys [form-errors form-params] :as request}]
(fc/start-form (or form-params {}) form-errors
(modal-response
(com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas
{}
[:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start)
:class "h-full w-full"}
[:fieldset {:class "hx-disable h-full w-full"}
(com/modal-card {}
[:div.m-2 "New job"]
[:div.space-y-6
(modal-response
(com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas
{}
[:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start)
:class "h-full w-full"}
[:fieldset {:class "hx-disable h-full w-full"}
(com/modal-card {}
[:div.m-2 "New job"]
[:div.space-y-6
(fc/with-field :name
(com/validated-field {:label "Job"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:value (fc/field-value)
:class "w-64"
:options [["" ""]
["yodlee2" "Yodlee Import"]
["yodlee2-accounts" "Yodlee Account Import"]
["intuit" "Intuit import"]
["plaid" "Plaid import"]
["bulk-journal-import" "Bulk Journal Import"]
["square-import-job" "Square Import"]
["register-invoice-import" "Register Invoice Import "]
["ezcater-upsert" "Upsert recent ezcater orders"]
["load-historical-sales" "Load Historical Square Sales"]
["export-backup" "Export Backup"]]
:hx-get (bidi/path-for ssr-routes/only-routes
:admin-job-subform)
:hx-target "#sub-form"
:hx-swap "innerHTML"})))
(fc/with-field :name
(com/validated-field {:label "Job"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:value (fc/field-value)
:class "w-64"
:options [["" ""]
["yodlee2" "Yodlee Import"]
["yodlee2-accounts" "Yodlee Account Import"]
["intuit" "Intuit import"]
["plaid" "Plaid import"]
["bulk-journal-import" "Bulk Journal Import"]
["square-import-job" "Square Import"]
["register-invoice-import" "Register Invoice Import "]
["ezcater-upsert" "Upsert recent ezcater orders"]
["load-historical-sales" "Load Historical Square Sales"]
["sales-summaries" "Refresh Sales Summaries"]
["export-backup" "Export Backup"]]
:hx-get (bidi/path-for ssr-routes/only-routes
:admin-job-subform)
:hx-target "#sub-form"
:hx-swap "innerHTML"})))
[:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))}) ]]
[:div
(com/form-errors {:errors (:errors fc/*form-errors*)})
(com/validated-save-button {:errors form-errors} "Run job")])]]))))
[:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))})]]
[:div
(com/form-errors {:errors (:errors fc/*form-errors*)})
(com/validated-save-button {:errors form-errors} "Run job")])]]))))
(def form-schema (mc/schema [:map
[:name [:string {:min 1}]]
[:ledger-url {:optional true} [:string {:min 1}]]
[:invoice-url {:optional true} [:string {:min 1}]]
[:client {:optional true} entity-id]
[:days {:optional true} [:int {:min 1 :max 120}]]
]))
[:days {:optional true} [:int {:min 1 :max 120}]]]))
(def key->handler
(apply-middleware-to-all-handlers
(->>
{:admin-jobs (helper/page-route grid-page)
:admin-job-table (helper/table-route grid-page)
:admin-job-subform (-> subform (wrap-schema-enforce :query-schema [:map [:name {:optional true} [:maybe :string]]]))
:admin-job-start (-> job-start
(wrap-schema-enforce :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 job-start-dialog))
:admin-job-start-dialog job-start-dialog})
(fn [h]
(-> h
(wrap-admin)
(wrap-client-redirect-unauthenticated)))))
(apply-middleware-to-all-handlers
(->>
{:admin-jobs (helper/page-route grid-page)
:admin-job-table (helper/table-route grid-page)
:admin-job-subform (-> subform (wrap-schema-enforce :query-schema [:map [:name {:optional true} [:maybe :string]]]))
:admin-job-start (-> job-start
(wrap-schema-enforce :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 job-start-dialog))
:admin-job-start-dialog job-start-dialog})
(fn [h]
(-> h
(wrap-admin)
(wrap-client-redirect-unauthenticated)))))

View File

@@ -4,6 +4,7 @@
:refer [apply-pagination apply-sort-3 conn merge-query pull-many
query2]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.datomic.sales-summaries :refer [total-credits total-debits]]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
[auto-ap.client-routes :as client-routes]
@@ -116,18 +117,6 @@
(defn sort-items [ss]
(sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss))
(defn total-debits [items]
(->> items
(filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)))
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0)))
(defn total-credits [items]
(->> items
(filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)))
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0)))
(defn truncate [s max-len]
(if (> (count s) max-len)
(str (subs s 0 (- max-len 3)) "...")
@@ -158,13 +147,13 @@
[:span.text-sm account-name]
(com/pill {:color :red} "Missing acct"))
(com/a-icon-button {:class "p-1"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account)
:hx-target "closest .account-cell"
:hx-swap "outerHTML"
:hx-vals (hx/json {:item-index (or (:item-index item) 0)
:client-id client-id
:current-account-id (or account-id "")})}
svg/pencil)]))
:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account)
:hx-target "closest .account-cell"
:hx-swap "outerHTML"
:hx-vals (hx/json {:item-index (or (:item-index item) 0)
:client-id client-id
:current-account-id (or account-id "")})}
svg/pencil)]))
(defn account-edit-cell [{:keys [field-name-prefix client-id current-account-id]}]
(let [account-input-name (str field-name-prefix "[ledger-mapped/account]")]
@@ -172,23 +161,23 @@
(account-typeahead* {:name account-input-name
:value current-account-id
:client-id client-id})
[:div.flex.gap-1
(com/a-icon-button {:class "p-1"
:hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account)
:hx-target "closest .account-cell"
:hx-swap "outerHTML"
:hx-include "closest .account-cell"
:hx-vals (hx/json {:field-name-prefix field-name-prefix
:client-id client-id})}
svg/check)
(com/a-icon-button {:class "p-1"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account)
:hx-target "closest .account-cell"
:hx-swap "outerHTML"
:hx-vals (hx/json {:field-name-prefix field-name-prefix
:client-id client-id
:current-account-id (or current-account-id "")})}
svg/x)]]))
[:div.flex.gap-1
(com/a-icon-button {:class "p-1"
:hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account)
:hx-target "closest .account-cell"
:hx-swap "outerHTML"
:hx-include "closest .account-cell"
:hx-vals (hx/json {:field-name-prefix field-name-prefix
:client-id client-id})}
svg/check)
(com/a-icon-button {:class "p-1"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account)
:hx-target "closest .account-cell"
:hx-swap "outerHTML"
:hx-vals (hx/json {:field-name-prefix field-name-prefix
:client-id client-id
:current-account-id (or current-account-id "")})}
svg/x)]]))
(def grid-page
(helper/build {:id "entity-table"
@@ -576,8 +565,8 @@
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
(account-display-cell {:item (assoc item :item-index actual-idx)
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
:client-id client-id})
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
:client-id client-id})
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
[:div.h-6]))]
[:div.mt-2.border-t.pt-1
(summary-total-display request)
@@ -619,13 +608,13 @@
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
(account-display-cell {:item (assoc item :item-index actual-idx)
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
:client-id client-id})
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
:client-id client-id})
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
[:div.h-6]))]
[:div.mt-2.border-t.pt-1
(summary-total-display request)
(unbalanced-display request)]]]
[:div.mt-4.border-t.pt-2
[:div.mt-4.border-t.pt-2
(fc/with-field :sales-summary/items
(com/data-grid-new-row {:colspan 2
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
@@ -761,16 +750,16 @@
::route/edit-wizard-navigate (-> mm/next-handler
(mm/wrap-wizard edit-wizard)
(mm/wrap-decode-multi-form-state))
::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items]
(fn render [cursor request]
(sales-summary-item-row*
{:value cursor
:client-id (:client-id (:query-params request))}))
(fn build-new-row [base _]
(assoc base :sales-summary-item/manual? true)))
(wrap-schema-enforce :query-schema [:map
[:client-id {:optional true}
[:maybe entity-id]]]))
::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items]
(fn render [cursor request]
(sales-summary-item-row*
{:value cursor
:client-id (:client-id (:query-params request))}))
(fn build-new-row [base _]
(assoc base :sales-summary-item/manual? true)))
(wrap-schema-enforce :query-schema [:map
[:client-id {:optional true}
[:maybe entity-id]]]))
::route/edit-item-account (-> edit-item-account
(wrap-schema-enforce :query-schema [:map
[:item-index nat-int?]

View File

@@ -386,6 +386,20 @@ module "close_auto_invoices_job" {
cpu = 512
}
module "sales_summaries_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(1 day)"
job_name = "sales-summaries"
execution_role_arn = var.execution_role_arn
use_schedule = true
memory = 4096
cpu = 2048
}
module "yodlee2_accounts_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"

View File

@@ -1,5 +1,5 @@
aws_access_key_id="AKIAINHACMVQJ6NYD26A"
aws_secret_access_key="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"
aws_access_key_id="AKIAZ4TSKSJ27WXFCOWK"
aws_secret_access_key="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"
domain="app.integreatconsult.com"
invoice_address="invoices@mail.app.integreatconsult.com"
base_url="https://app.integreatconsult.com"

View File

@@ -1,7 +1,7 @@
{
"version": 4,
"terraform_version": "1.9.2",
"serial": 718,
"terraform_version": "1.15.1",
"serial": 722,
"lineage": "9b630886-8cee-a57d-c7a2-4f19f13f9c51",
"outputs": {
"aws_access_key_id": {
@@ -115,7 +115,8 @@
"usage_operation": "RunInstances",
"virtualization_type": "hvm"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -133,7 +134,8 @@
"id": "679918342773",
"user_id": "AIDAJPUJFTOKO4IRADMV4"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -181,7 +183,8 @@
],
"version": "2012-10-17"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -246,6 +249,7 @@
}
]
],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -311,6 +315,7 @@
}
]
],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -346,6 +351,7 @@
"type": "gp2"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjMwMDAwMDAwMDAwMH19",
"dependencies": [
"aws_instance.solr_ec2",
@@ -445,6 +451,7 @@
"wait_for_steady_state": true
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxMjAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_ecs_task_definition.integreat_app",
@@ -535,6 +542,7 @@
"wait_for_steady_state": true
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxMjAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_ecs_task_definition.solr",
@@ -580,6 +588,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -636,6 +645,7 @@
]
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"aws_efs_file_system.solr_storage"
@@ -681,6 +691,7 @@
"throughput_mode": "bursting"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -719,6 +730,7 @@
}
]
],
"identity_schema_version": 0,
"dependencies": [
"aws_iam_user.app_user"
]
@@ -744,7 +756,8 @@
"tags_all": {},
"unique_id": "AIDAINFBWI2I7A3TKPGW2"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -762,6 +775,7 @@
"user": "integreat-prod"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"dependencies": [
"aws_iam_user.app_user"
]
@@ -908,6 +922,7 @@
]
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwidXBkYXRlIjo2MDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"data.aws_ami.amazon_linux_2023"
@@ -1014,6 +1029,7 @@
"zone_id": "Z35SXDOTRQ7X7K"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19"
}
]
@@ -1063,6 +1079,7 @@
}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsicmVhZCI6NjAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_lb.integreat_app"
@@ -1106,6 +1123,7 @@
}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsicmVhZCI6NjAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_acm_certificate.cert",
@@ -1173,6 +1191,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA==",
"dependencies": [
"aws_acm_certificate.cert",
@@ -1242,6 +1261,7 @@
"vpc_id": "vpc-b5b7d6ce"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1320,6 +1340,7 @@
"website_endpoint": "data.prod.app.integreatconsult.com.s3-website-us-east-1.amazonaws.com"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1391,6 +1412,7 @@
"website_endpoint": null
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA==",
"dependencies": [
"data.aws_caller_identity.current"
@@ -1489,6 +1511,7 @@
"website_endpoint": "app.integreatconsult.com.s3-website-us-east-1.amazonaws.com"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1560,6 +1583,7 @@
"website_endpoint": null
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjM2MDAwMDAwMDAwMDAsInJlYWQiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19"
}
]
@@ -1591,6 +1615,7 @@
"topic": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"dependencies": [
"aws_s3_bucket.invoices",
"aws_sqs_queue.integreat-mail",
@@ -1613,6 +1638,7 @@
"policy": "{\"Statement\":[{\"Action\":\"s3:*\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"arn:aws:iam::679918342773:role/http-proxy\",\"arn:aws:iam::679918342773:role/datomic-ddb\"]},\"Resource\":[\"arn:aws:s3:::toast.prod.app.integreatconsult.com/*\",\"arn:aws:s3:::toast.prod.app.integreatconsult.com\"],\"Sid\":\"\"}],\"Version\":\"2012-10-17\"}"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA==",
"dependencies": [
"aws_s3_bucket.toast_bucket",
@@ -1638,6 +1664,7 @@
"service_id": "srv-ren22oppkwwryqqr"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA==",
"dependencies": [
"aws_instance.solr_ec2",
@@ -1685,6 +1712,7 @@
"type": "DNS_HTTP"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1727,6 +1755,7 @@
"type": "DNS_HTTP"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1769,6 +1798,7 @@
"type": "DNS_HTTP"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1810,6 +1840,7 @@
"workmail_action": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"dependencies": [
"aws_s3_bucket.invoices",
"aws_ses_receipt_rule_set.main",
@@ -1831,7 +1862,8 @@
"id": "default-rule-set",
"rule_set_name": "default-rule-set"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -1868,6 +1900,7 @@
"visibility_timeout_seconds": 30
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1905,6 +1938,7 @@
"visibility_timeout_seconds": 30
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"dependencies": [
"aws_s3_bucket.invoices",
"data.aws_caller_identity.current"
@@ -1945,6 +1979,7 @@
"visibility_timeout_seconds": 30
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1968,6 +2003,7 @@
"volume_id": "vol-0069283d41ff6c010"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_ebs_volume.solr_ec2_storage",
@@ -2014,6 +2050,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2043,6 +2080,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2107,6 +2145,7 @@
"target_id": "close-auto-invoices"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.close_auto_invoices_job.aws_cloudwatch_event_rule.schedule",
@@ -2152,6 +2191,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2181,6 +2221,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2245,6 +2286,7 @@
"target_id": "import-uploaded-invoices"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.import_uploaded_invoices_job.aws_cloudwatch_event_rule.schedule",
@@ -2290,6 +2332,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2319,6 +2362,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2383,6 +2427,7 @@
"target_id": "insight-outcome-recommendation"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.insight_outcome_recommendation_job.aws_cloudwatch_event_rule.schedule",
@@ -2428,6 +2473,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2457,6 +2503,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2521,6 +2568,7 @@
"target_id": "intuit"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.intuit_job.aws_cloudwatch_event_rule.schedule",
@@ -2566,6 +2614,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2607,6 +2656,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2636,6 +2686,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2700,6 +2751,7 @@
"target_id": "ntg"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.ntg_job.aws_cloudwatch_event_rule.schedule",
@@ -2745,6 +2797,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2774,6 +2827,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2838,6 +2892,7 @@
"target_id": "plaid"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.plaid_job.aws_cloudwatch_event_rule.schedule",
@@ -2883,6 +2938,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2912,6 +2968,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2976,6 +3033,7 @@
"target_id": "reconcile-ledger"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.reconcile_ledger_job.aws_cloudwatch_event_rule.schedule",
@@ -3021,6 +3079,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3062,6 +3121,148 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
},
{
"module": "module.sales_summaries_job[0]",
"mode": "managed",
"type": "aws_cloudwatch_event_rule",
"name": "schedule",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"index_key": 0,
"schema_version": 0,
"attributes": {
"arn": "arn:aws:events:us-east-1:679918342773:rule/sales-summaries-schedule-prod",
"description": "",
"event_bus_name": "default",
"event_pattern": null,
"id": "sales-summaries-schedule-prod",
"is_enabled": true,
"name": "sales-summaries-schedule-prod",
"name_prefix": "",
"role_arn": "",
"schedule_expression": "rate(1 day)",
"tags": null,
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
},
{
"module": "module.sales_summaries_job[0]",
"mode": "managed",
"type": "aws_cloudwatch_event_target",
"name": "job_target",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"index_key": 0,
"schema_version": 1,
"attributes": {
"arn": "arn:aws:ecs:us-east-1:679918342773:cluster/default",
"batch_target": [],
"dead_letter_config": [],
"ecs_target": [
{
"capacity_provider_strategy": [],
"enable_ecs_managed_tags": false,
"enable_execute_command": false,
"group": "",
"launch_type": "FARGATE",
"network_configuration": [
{
"assign_public_ip": true,
"security_groups": [
"sg-004e5855310c453a3",
"sg-02d167406b1082698"
],
"subnets": [
"subnet-5e675761",
"subnet-8519fde2",
"subnet-89bab8d4"
]
}
],
"ordered_placement_strategy": [],
"placement_constraint": [],
"platform_version": "",
"propagate_tags": "TASK_DEFINITION",
"tags": null,
"task_count": 1,
"task_definition_arn": "arn:aws:ecs:us-east-1:679918342773:task-definition/sales_summaries_prod:1"
}
],
"event_bus_name": "default",
"http_target": [],
"id": "sales-summaries-schedule-prod-sales-summaries",
"input": "",
"input_path": "",
"input_transformer": [],
"kinesis_target": [],
"redshift_target": [],
"retry_policy": [],
"role_arn": "arn:aws:iam::679918342773:role/service-role/Amazon_EventBridge_Invoke_ECS_1758992733",
"rule": "sales-summaries-schedule-prod",
"run_command_targets": [],
"sqs_target": [],
"target_id": "sales-summaries"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.sales_summaries_job.aws_cloudwatch_event_rule.schedule",
"module.sales_summaries_job.aws_ecs_task_definition.background_taskdef"
]
}
]
},
{
"module": "module.sales_summaries_job[0]",
"mode": "managed",
"type": "aws_ecs_task_definition",
"name": "background_taskdef",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"arn": "arn:aws:ecs:us-east-1:679918342773:task-definition/sales_summaries_prod:1",
"arn_without_revision": "arn:aws:ecs:us-east-1:679918342773:task-definition/sales_summaries_prod",
"container_definitions": "[{\"cpu\":0,\"dockerLabels\":{\"com.datadoghq.tags.env\":\"prod\",\"com.datadoghq.tags.service\":\"sales-summaries\"},\"environment\":[{\"name\":\"DD_CONTAINER_ENV_AS_TAGS\",\"value\":\"{\\\"INTEGREAT_JOB\\\":\\\"background_job\\\"}\"},{\"name\":\"DD_ENV\",\"value\":\"prod\"},{\"name\":\"DD_SERVICE\",\"value\":\"sales-summaries\"},{\"name\":\"INTEGREAT_JOB\",\"value\":\"sales-summaries\"},{\"name\":\"config\",\"value\":\"/usr/local/config/prod-background-worker.edn\"}],\"essential\":true,\"image\":\"679918342773.dkr.ecr.us-east-1.amazonaws.com/integreat-cloud:prod\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"/ecs/integreat-app-prod\",\"awslogs-region\":\"us-east-1\",\"awslogs-stream-prefix\":\"ecs\"}},\"mountPoints\":[],\"name\":\"integreat-app\",\"portMappings\":[{\"containerPort\":9000,\"hostPort\":9000,\"protocol\":\"tcp\"},{\"containerPort\":9090,\"hostPort\":9090,\"protocol\":\"tcp\"}],\"systemControls\":[],\"volumesFrom\":[]},{\"cpu\":0,\"environment\":[{\"name\":\"DD_API_KEY\",\"value\":\"ce10d932c47b358e81081ae67bd8c112\"},{\"name\":\"ECS_FARGATE\",\"value\":\"true\"}],\"essential\":true,\"image\":\"public.ecr.aws/datadog/agent:latest\",\"mountPoints\":[],\"name\":\"datadog-agent\",\"portMappings\":[],\"systemControls\":[],\"volumesFrom\":[]}]",
"cpu": "2048",
"ephemeral_storage": [],
"execution_role_arn": "arn:aws:iam::679918342773:role/ecsTaskExecutionRole",
"family": "sales_summaries_prod",
"id": "sales_summaries_prod",
"inference_accelerator": [],
"ipc_mode": "",
"memory": "4096",
"network_mode": "awsvpc",
"pid_mode": "",
"placement_constraints": [],
"proxy_configuration": [],
"requires_compatibilities": [
"FARGATE"
],
"revision": 1,
"runtime_platform": [],
"skip_destroy": false,
"tags": null,
"tags_all": {},
"task_role_arn": "arn:aws:iam::679918342773:role/datomic-ddb",
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3091,6 +3292,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -3155,6 +3357,7 @@
"target_id": "square-import-job"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.square_import_job.aws_cloudwatch_event_rule.schedule",
@@ -3200,6 +3403,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3229,6 +3433,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -3293,6 +3498,7 @@
"target_id": "sysco"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.sysco_job.aws_cloudwatch_event_rule.schedule",
@@ -3338,6 +3544,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3367,6 +3574,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -3431,6 +3639,7 @@
"target_id": "vendor-usages"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.vendor_usages_job.aws_cloudwatch_event_rule.schedule",
@@ -3476,6 +3685,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3517,6 +3727,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3546,6 +3757,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -3610,6 +3822,7 @@
"target_id": "yodlee2"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.yodlee2_job.aws_cloudwatch_event_rule.schedule",
@@ -3655,6 +3868,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]

View File

@@ -1,7 +1,7 @@
{
"version": 4,
"terraform_version": "1.9.2",
"serial": 714,
"terraform_version": "1.15.1",
"serial": 718,
"lineage": "9b630886-8cee-a57d-c7a2-4f19f13f9c51",
"outputs": {
"aws_access_key_id": {
@@ -115,7 +115,8 @@
"usage_operation": "RunInstances",
"virtualization_type": "hvm"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -133,7 +134,8 @@
"id": "679918342773",
"user_id": "AIDAJPUJFTOKO4IRADMV4"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -181,7 +183,8 @@
],
"version": "2012-10-17"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -246,6 +249,7 @@
}
]
],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -311,6 +315,7 @@
}
]
],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -346,6 +351,7 @@
"type": "gp2"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwLCJ1cGRhdGUiOjMwMDAwMDAwMDAwMH19",
"dependencies": [
"aws_instance.solr_ec2",
@@ -435,7 +441,7 @@
],
"tags": {},
"tags_all": {},
"task_definition": "arn:aws:ecs:us-east-1:679918342773:task-definition/integreat_app_prod:837",
"task_definition": "arn:aws:ecs:us-east-1:679918342773:task-definition/integreat_app_prod:841",
"timeouts": {
"create": null,
"delete": null,
@@ -445,6 +451,7 @@
"wait_for_steady_state": true
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxMjAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_ecs_task_definition.integreat_app",
@@ -535,6 +542,7 @@
"wait_for_steady_state": true
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiZGVsZXRlIjoxMjAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_ecs_task_definition.solr",
@@ -580,6 +588,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -636,6 +645,7 @@
]
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"aws_efs_file_system.solr_storage"
@@ -667,9 +677,9 @@
"provisioned_throughput_in_mibps": 0,
"size_in_bytes": [
{
"value": 1432420352,
"value": 1434062848,
"value_in_ia": 0,
"value_in_standard": 1432420352
"value_in_standard": 1434062848
}
],
"tags": {
@@ -681,6 +691,7 @@
"throughput_mode": "bursting"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -719,6 +730,7 @@
}
]
],
"identity_schema_version": 0,
"dependencies": [
"aws_iam_user.app_user"
]
@@ -744,7 +756,8 @@
"tags_all": {},
"unique_id": "AIDAINFBWI2I7A3TKPGW2"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -762,6 +775,7 @@
"user": "integreat-prod"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"dependencies": [
"aws_iam_user.app_user"
]
@@ -908,6 +922,7 @@
]
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwidXBkYXRlIjo2MDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"data.aws_ami.amazon_linux_2023"
@@ -1014,6 +1029,7 @@
"zone_id": "Z35SXDOTRQ7X7K"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19"
}
]
@@ -1063,6 +1079,7 @@
}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsicmVhZCI6NjAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_lb.integreat_app"
@@ -1106,6 +1123,7 @@
}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsicmVhZCI6NjAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_acm_certificate.cert",
@@ -1173,6 +1191,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA==",
"dependencies": [
"aws_acm_certificate.cert",
@@ -1242,6 +1261,7 @@
"vpc_id": "vpc-b5b7d6ce"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1320,6 +1340,7 @@
"website_endpoint": "data.prod.app.integreatconsult.com.s3-website-us-east-1.amazonaws.com"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1391,6 +1412,7 @@
"website_endpoint": null
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA==",
"dependencies": [
"data.aws_caller_identity.current"
@@ -1489,6 +1511,7 @@
"website_endpoint": "app.integreatconsult.com.s3-website-us-east-1.amazonaws.com"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1560,6 +1583,7 @@
"website_endpoint": null
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjM2MDAwMDAwMDAwMDAsInJlYWQiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19"
}
]
@@ -1591,6 +1615,7 @@
"topic": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"dependencies": [
"aws_s3_bucket.invoices",
"aws_sqs_queue.integreat-mail",
@@ -1613,6 +1638,7 @@
"policy": "{\"Statement\":[{\"Action\":\"s3:*\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"arn:aws:iam::679918342773:role/http-proxy\",\"arn:aws:iam::679918342773:role/datomic-ddb\"]},\"Resource\":[\"arn:aws:s3:::toast.prod.app.integreatconsult.com/*\",\"arn:aws:s3:::toast.prod.app.integreatconsult.com\"],\"Sid\":\"\"}],\"Version\":\"2012-10-17\"}"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA==",
"dependencies": [
"aws_s3_bucket.toast_bucket",
@@ -1638,6 +1664,7 @@
"service_id": "srv-ren22oppkwwryqqr"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA==",
"dependencies": [
"aws_instance.solr_ec2",
@@ -1685,6 +1712,7 @@
"type": "DNS_HTTP"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1727,6 +1755,7 @@
"type": "DNS_HTTP"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1769,6 +1798,7 @@
"type": "DNS_HTTP"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1810,6 +1840,7 @@
"workmail_action": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"dependencies": [
"aws_s3_bucket.invoices",
"aws_ses_receipt_rule_set.main",
@@ -1831,7 +1862,8 @@
"id": "default-rule-set",
"rule_set_name": "default-rule-set"
},
"sensitive_attributes": []
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
},
@@ -1868,6 +1900,7 @@
"visibility_timeout_seconds": 30
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1905,6 +1938,7 @@
"visibility_timeout_seconds": 30
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"dependencies": [
"aws_s3_bucket.invoices",
"data.aws_caller_identity.current"
@@ -1945,6 +1979,7 @@
"visibility_timeout_seconds": 30
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -1968,6 +2003,7 @@
"volume_id": "vol-0069283d41ff6c010"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDAsImRlbGV0ZSI6MzAwMDAwMDAwMDAwfX0=",
"dependencies": [
"aws_ebs_volume.solr_ec2_storage",
@@ -2014,6 +2050,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2043,6 +2080,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2107,6 +2145,7 @@
"target_id": "close-auto-invoices"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.close_auto_invoices_job.aws_cloudwatch_event_rule.schedule",
@@ -2152,144 +2191,7 @@
"volume": []
},
"sensitive_attributes": [],
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
},
{
"module": "module.current_balance_cache[0]",
"mode": "managed",
"type": "aws_cloudwatch_event_rule",
"name": "schedule",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"index_key": 0,
"schema_version": 0,
"attributes": {
"arn": "arn:aws:events:us-east-1:679918342773:rule/current-balance-cache-schedule-prod",
"description": "",
"event_bus_name": "default",
"event_pattern": null,
"id": "current-balance-cache-schedule-prod",
"is_enabled": true,
"name": "current-balance-cache-schedule-prod",
"name_prefix": "",
"role_arn": "",
"schedule_expression": "rate(30 minutes)",
"tags": {},
"tags_all": {}
},
"sensitive_attributes": [],
"private": "bnVsbA=="
}
]
},
{
"module": "module.current_balance_cache[0]",
"mode": "managed",
"type": "aws_cloudwatch_event_target",
"name": "job_target",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"index_key": 0,
"schema_version": 1,
"attributes": {
"arn": "arn:aws:ecs:us-east-1:679918342773:cluster/default",
"batch_target": [],
"dead_letter_config": [],
"ecs_target": [
{
"capacity_provider_strategy": [],
"enable_ecs_managed_tags": false,
"enable_execute_command": false,
"group": "",
"launch_type": "FARGATE",
"network_configuration": [
{
"assign_public_ip": true,
"security_groups": [
"sg-004e5855310c453a3",
"sg-02d167406b1082698"
],
"subnets": [
"subnet-5e675761",
"subnet-8519fde2",
"subnet-89bab8d4"
]
}
],
"ordered_placement_strategy": [],
"placement_constraint": [],
"platform_version": "",
"propagate_tags": "TASK_DEFINITION",
"tags": {},
"task_count": 1,
"task_definition_arn": "arn:aws:ecs:us-east-1:679918342773:task-definition/current_balance_cache_prod:3"
}
],
"event_bus_name": "default",
"http_target": [],
"id": "current-balance-cache-schedule-prod-current-balance-cache",
"input": "",
"input_path": "",
"input_transformer": [],
"kinesis_target": [],
"redshift_target": [],
"retry_policy": [],
"role_arn": "arn:aws:iam::679918342773:role/service-role/Amazon_EventBridge_Invoke_ECS_1758992733",
"rule": "current-balance-cache-schedule-prod",
"run_command_targets": [],
"sqs_target": [],
"target_id": "current-balance-cache"
},
"sensitive_attributes": [],
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.current_balance_cache.aws_cloudwatch_event_rule.schedule",
"module.current_balance_cache.aws_ecs_task_definition.background_taskdef"
]
}
]
},
{
"module": "module.current_balance_cache[0]",
"mode": "managed",
"type": "aws_ecs_task_definition",
"name": "background_taskdef",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"arn": "arn:aws:ecs:us-east-1:679918342773:task-definition/current_balance_cache_prod:3",
"arn_without_revision": "arn:aws:ecs:us-east-1:679918342773:task-definition/current_balance_cache_prod",
"container_definitions": "[{\"cpu\":0,\"dockerLabels\":{\"com.datadoghq.tags.env\":\"prod\",\"com.datadoghq.tags.service\":\"current-balance-cache\"},\"environment\":[{\"name\":\"DD_CONTAINER_ENV_AS_TAGS\",\"value\":\"{\\\"INTEGREAT_JOB\\\":\\\"background_job\\\"}\"},{\"name\":\"DD_ENV\",\"value\":\"prod\"},{\"name\":\"DD_SERVICE\",\"value\":\"current-balance-cache\"},{\"name\":\"INTEGREAT_JOB\",\"value\":\"current-balance-cache\"},{\"name\":\"config\",\"value\":\"/usr/local/config/prod-background-worker.edn\"}],\"essential\":true,\"image\":\"679918342773.dkr.ecr.us-east-1.amazonaws.com/integreat-cloud:prod\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"/ecs/integreat-app-prod\",\"awslogs-region\":\"us-east-1\",\"awslogs-stream-prefix\":\"ecs\"}},\"mountPoints\":[],\"name\":\"integreat-app\",\"portMappings\":[{\"containerPort\":9000,\"hostPort\":9000,\"protocol\":\"tcp\"},{\"containerPort\":9090,\"hostPort\":9090,\"protocol\":\"tcp\"}],\"systemControls\":[],\"volumesFrom\":[]},{\"cpu\":0,\"environment\":[{\"name\":\"DD_API_KEY\",\"value\":\"ce10d932c47b358e81081ae67bd8c112\"},{\"name\":\"ECS_FARGATE\",\"value\":\"true\"}],\"essential\":true,\"image\":\"public.ecr.aws/datadog/agent:latest\",\"mountPoints\":[],\"name\":\"datadog-agent\",\"portMappings\":[],\"systemControls\":[],\"volumesFrom\":[]}]",
"cpu": "512",
"ephemeral_storage": [],
"execution_role_arn": "arn:aws:iam::679918342773:role/ecsTaskExecutionRole",
"family": "current_balance_cache_prod",
"id": "current_balance_cache_prod",
"inference_accelerator": [],
"ipc_mode": "",
"memory": "2048",
"network_mode": "awsvpc",
"pid_mode": "",
"placement_constraints": [],
"proxy_configuration": [],
"requires_compatibilities": [
"FARGATE"
],
"revision": 3,
"runtime_platform": [],
"skip_destroy": false,
"tags": {},
"tags_all": {},
"task_role_arn": "arn:aws:iam::679918342773:role/datomic-ddb",
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2319,6 +2221,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2383,6 +2286,7 @@
"target_id": "import-uploaded-invoices"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.import_uploaded_invoices_job.aws_cloudwatch_event_rule.schedule",
@@ -2428,6 +2332,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2457,6 +2362,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2521,6 +2427,7 @@
"target_id": "insight-outcome-recommendation"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.insight_outcome_recommendation_job.aws_cloudwatch_event_rule.schedule",
@@ -2566,6 +2473,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2595,6 +2503,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2659,6 +2568,7 @@
"target_id": "intuit"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.intuit_job.aws_cloudwatch_event_rule.schedule",
@@ -2704,6 +2614,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2745,6 +2656,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2774,6 +2686,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2838,6 +2751,7 @@
"target_id": "ntg"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.ntg_job.aws_cloudwatch_event_rule.schedule",
@@ -2883,6 +2797,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -2912,6 +2827,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -2976,6 +2892,7 @@
"target_id": "plaid"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.plaid_job.aws_cloudwatch_event_rule.schedule",
@@ -3021,6 +2938,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3050,6 +2968,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -3114,6 +3033,7 @@
"target_id": "reconcile-ledger"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.reconcile_ledger_job.aws_cloudwatch_event_rule.schedule",
@@ -3159,6 +3079,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3200,6 +3121,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3229,6 +3151,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -3293,6 +3216,7 @@
"target_id": "square-import-job"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.square_import_job.aws_cloudwatch_event_rule.schedule",
@@ -3338,6 +3262,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3367,6 +3292,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -3431,6 +3357,7 @@
"target_id": "sysco"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.sysco_job.aws_cloudwatch_event_rule.schedule",
@@ -3476,6 +3403,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3505,6 +3433,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -3569,6 +3498,7 @@
"target_id": "vendor-usages"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.vendor_usages_job.aws_cloudwatch_event_rule.schedule",
@@ -3614,6 +3544,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3655,6 +3586,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
@@ -3684,6 +3616,7 @@
"tags_all": {}
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
@@ -3748,6 +3681,7 @@
"target_id": "yodlee2"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [
"module.yodlee2_job.aws_cloudwatch_event_rule.schedule",
@@ -3793,6 +3727,7 @@
"volume": []
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]

View File

@@ -0,0 +1,149 @@
(ns auto-ap.jobs.rekey-square-external-ids-test
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.integration.util :refer [setup-test-data wrap-setup]]
[auto-ap.jobs.rekey-square-external-ids :as sut]
[clojure.string]
[clojure.test :refer [deftest is testing use-fixtures]]
[datomic.api :as dc]))
(use-fixtures :each wrap-setup)
(def sales-date #inst "2026-08-01T07:00:00.000-00:00")
(defn- charge-count []
(count (dc/q '[:find ?e :where [?e :charge/external-id]] (dc/db conn))))
(defn- charges-of [order]
(->> (dc/datoms (dc/db conn) :eavt order :sales-order/charges)
(map :v)
(map (fn [c] {:eid c
:key (:v (first (dc/datoms (dc/db conn) :eavt c :charge/external-id)))
:total (:v (first (dc/datoms (dc/db conn) :eavt c :charge/total)))
:tip (:v (first (dc/datoms (dc/db conn) :eavt c :charge/tip)))}))
vec))
(defn- parents-of [charge]
(reduce (fn [n _] (inc n)) 0 (dc/datoms (dc/db conn) :vaet charge :sales-order/charges)))
(defn- two-orders-sharing-one-charge []
(let [{:strs [test-client-id]} (setup-test-data [])
other (get-in @(dc/transact conn [{:db/id "other" :client/code "NGCC"}]) [:tempids "other"])
tx @(dc/transact conn [{:db/id "charge"
:charge/external-id "square/charge/shared1"
:charge/type-name "CARD"
:charge/total 120.0
:charge/tip 20.0}
{:db/id "order-a"
:sales-order/external-id "square/order/NGCD-CD-o1"
:sales-order/client test-client-id
:sales-order/location "CD"
:sales-order/date sales-date
:sales-order/charges ["charge"]}
{:db/id "order-b"
:sales-order/external-id "square/order/NGCC-CC-o1"
:sales-order/client other
:sales-order/location "CC"
:sales-order/date sales-date
:sales-order/charges ["charge"]}])]
{:order-a (get-in tx [:tempids "order-a"])
:order-b (get-in tx [:tempids "order-b"])
:charge (get-in tx [:tempids "charge"])
:code-a (:client/code (dc/entity (dc/db conn) test-client-id))
:code-b "NGCC"}))
(deftest a-shared-charge-starts-with-two-parents
(testing "the condition under test really exists before the split runs"
(let [{:keys [charge]} (two-orders-sharing-one-charge)]
(is (= 1 (charge-count)))
(is (= 2 (parents-of charge))
"one charge entity, referenced by both clients' orders"))))
(deftest split-gives-each-order-its-own-charge
(testing "each order ends up with its own charge, scoped to its own client, carrying the same
amounts — so the component relationship means what it says and retracting one order
cannot delete the other's payment"
(let [{:keys [order-a order-b code-a code-b]} (two-orders-sharing-one-charge)
result (sut/split-and-rekey-charges! [order-a order-b] 100)]
(is (= {:rekeyed 1 :cloned 1} result) "first order keeps it, second gets a copy")
(is (= 2 (charge-count)) "exactly one new entity was created")
(let [a (charges-of order-a)
b (charges-of order-b)]
(is (= 1 (count a)))
(is (= 1 (count b)))
(is (= (str "square/charge/" code-a "-CD-shared1") (:key (first a))))
(is (= (str "square/charge/" code-b "-CC-shared1") (:key (first b))))
(is (not= (:eid (first a)) (:eid (first b))) "two distinct entities")
(is (= 120.0 (:total (first a)) (:total (first b))) "amounts copied")
(is (= 20.0 (:tip (first a)) (:tip (first b))) "tips copied")
(is (= 1 (parents-of (:eid (first a)))))
(is (= 1 (parents-of (:eid (first b))))
"no charge has more than one parent order any more")))))
(deftest split-leaves-an-unshared-charge-alone
(testing "an order that already owns its charge outright is only re-keyed, never cloned"
(let [{:strs [test-client-id]} (setup-test-data [])
tx @(dc/transact conn [{:db/id "charge"
:charge/external-id "square/charge/solo1"
:charge/total 50.0}
{:db/id "order"
:sales-order/external-id "square/order/NGCD-CD-o2"
:sales-order/client test-client-id
:sales-order/location "CD"
:sales-order/date sales-date
:sales-order/charges ["charge"]}])
order (get-in tx [:tempids "order"])
code (:client/code (dc/entity (dc/db conn) test-client-id))]
(is (= {:rekeyed 1 :cloned 0} (sut/split-and-rekey-charges! [order] 100)))
(is (= 1 (charge-count)) "nothing was created")
(is (= (str "square/charge/" code "-CD-solo1") (:key (first (charges-of order))))))))
(deftest split-is-idempotent
(testing "re-running over an already-split database changes nothing further"
(let [{:keys [order-a order-b]} (two-orders-sharing-one-charge)]
(sut/split-and-rekey-charges! [order-a order-b] 100)
(let [after-first (charge-count)]
(is (= {:rekeyed 0 :cloned 0} (sut/split-and-rekey-charges! [order-a order-b] 100))
"every charge already carries the key its order expects, so there is nothing to do")
(is (= after-first (charge-count)) "and no further entities appear")))))
(deftest clone-is-not-double-scoped-across-batches
(testing "with a batch size of one, the second order sees a charge already carrying the first
client's scope. It must clone using the underlying Square id, not re-scope the scoped
key — otherwise the entity ends up keyed NGCD-CD-NGCC-CC-<id>, the importer computes
the correct key, misses, and creates a second charge that doubles the tender."
(let [{:keys [order-a order-b code-a code-b]} (two-orders-sharing-one-charge)]
(sut/split-and-rekey-charges! [order-a order-b] 1)
(let [ka (:key (first (charges-of order-a)))
kb (:key (first (charges-of order-b)))]
(is (= (str "square/charge/" code-a "-CD-shared1") ka))
(is (= (str "square/charge/" code-b "-CC-shared1") kb))
(is (not (clojure.string/includes? kb (str code-a "-CD")))
"the clone carries one scope, not two")
(is (= 2 (charge-count)))))))
(deftest two-orders-of-the-same-client-keep-sharing
(testing "one payment covering two of the SAME client's orders is left shared, on purpose.
There is no second name to give a copy — both orders compute the same one — and a copy
would double that client's takings for the day. The component cascade still reaches
these, which is why remove-voided-orders needs its own guard."
(let [{:strs [test-client-id]} (setup-test-data [])
tx @(dc/transact conn [{:db/id "charge"
:charge/external-id "square/charge/same1"
:charge/total 75.0}
{:db/id "o1" :sales-order/external-id "square/order/x-1"
:sales-order/client test-client-id :sales-order/location "CD"
:sales-order/date sales-date :sales-order/charges ["charge"]}
{:db/id "o2" :sales-order/external-id "square/order/x-2"
:sales-order/client test-client-id :sales-order/location "CD"
:sales-order/date sales-date :sales-order/charges ["charge"]}])
o1 (get-in tx [:tempids "o1"]) o2 (get-in tx [:tempids "o2"])
code (:client/code (dc/entity (dc/db conn) test-client-id))]
(is (= {:rekeyed 1 :cloned 0} (sut/split-and-rekey-charges! [o1 o2] 100))
"renamed once, not copied")
(is (= 1 (charge-count)) "no copy was made, so the takings are not doubled")
(is (= (str "square/charge/" code "-CD-same1") (:key (first (charges-of o1)))))
(is (= (:eid (first (charges-of o1))) (:eid (first (charges-of o2))))
"both orders still point at the one payment"))))

View File

@@ -0,0 +1,204 @@
(ns auto-ap.jobs.sales-summaries-test
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.datomic.sales-summaries :as d-ss]
[auto-ap.integration.util :refer [setup-test-data wrap-setup]]
[auto-ap.jobs.sales-summaries :as sut]
[clojure.test :refer [deftest is testing use-fixtures]]
[datomic.api :as dc]))
(use-fixtures :each wrap-setup)
(def sales-date #inst "2026-08-01T07:00:00.000-00:00")
(defn- order
"A sales order on `sales-date`, carrying whatever the case under test needs. The external id
is Square-shaped by default because `get-service-charges` falls back to it when an order has
no `:sales-order/vendor`."
[client id attrs]
(merge {:db/id (str "order-" id)
:sales-order/external-id (str "square/order/TEST-" id)
:sales-order/client client
:sales-order/date sales-date
:sales-order/total 100.0}
attrs))
(defn- charge [id attrs]
(merge {:db/id (str "charge-" id)
:charge/external-id (str "square/charge/" id)
:charge/type-name "CARD"
:charge/total 100.0}
attrs))
(defn- tip-for [client]
(:ledger-mapped/amount (#'sut/get-tip client sales-date)))
(defn- service-charges-for [client]
(#'sut/get-service-charges client sales-date))
(defn- enable-service-charges! [client]
@(dc/transact conn [{:db/id client
:client/feature-flags [sut/service-charges-flag]}]))
(deftest tip-counts-a-reversal-on-an-untendered-order
(testing "a return-only order has no tender to join through, so its negative tip must come
from the order or the day credits a tip that was handed back"
(let [{:strs [test-client-id]} (setup-test-data [])]
@(dc/transact conn [(order test-client-id "return-only" {:sales-order/tip -12.0})])
(is (= -12.0 (tip-for test-client-id))))))
(deftest tip-on-a-tendered-order-still-comes-from-the-tender
(testing "the tender carries a tip the order does not — auto-gratuity booked as a service
charge. Reading the order instead of the tender would drop it."
(let [{:strs [test-client-id]} (setup-test-data [])]
@(dc/transact conn [(order test-client-id "tendered"
{:sales-order/tip 0.0
:sales-order/charges [(charge "tendered" {:charge/tip 50.0})]})])
(is (= 50.0 (tip-for test-client-id))))))
(deftest tip-on-an-ordinary-order-is-counted-once
(testing "an order that agrees with its tender is not double counted by the additive form"
(let [{:strs [test-client-id]} (setup-test-data [])]
@(dc/transact conn [(order test-client-id "ordinary"
{:sales-order/tip 5.0
:sales-order/charges [(charge "ordinary" {:charge/tip 5.0})]})])
(is (= 5.0 (tip-for test-client-id))))))
(deftest service-charges-need-the-feature-flag
(testing "without the flag the summary behaves exactly as it does today"
(let [{:strs [test-client-id]} (setup-test-data [])]
@(dc/transact conn [(order test-client-id "square-sc"
{:sales-order/vendor :vendor/ccp-square
:sales-order/service-charge 50.0})])
(is (nil? (service-charges-for test-client-id))))))
(deftest service-charges-credit-square-orders
(testing "a service charge rides along in the tender, so it needs a credit to match"
(let [{:strs [test-client-id]} (setup-test-data [])]
(enable-service-charges! test-client-id)
@(dc/transact conn [(order test-client-id "square-sc"
{:sales-order/vendor :vendor/ccp-square
:sales-order/service-charge 50.0})])
(let [item (service-charges-for test-client-id)]
(is (= 50.0 (:ledger-mapped/amount item)))
(is (= :ledger-side/credit (:ledger-mapped/ledger-side item)))
(is (= "Service Charges" (:sales-summary-item/category item)))))))
(deftest service-charges-count-both-signs
(testing "a returned catering fee arrives as a negative service charge and is subtracted back
out of returns, so dropping negatives loses the reversal"
(let [{:strs [test-client-id]} (setup-test-data [])]
(enable-service-charges! test-client-id)
@(dc/transact conn [(order test-client-id "refunded-fee"
{:sales-order/vendor :vendor/ccp-square
:sales-order/service-charge -140.0})])
(is (= -140.0 (:ledger-mapped/amount (service-charges-for test-client-id)))))))
(deftest service-charges-exclude-non-square-vendors
(testing "ezCater service charges are commission deducted from the restaurant rather than
collected from the diner, so crediting them would make the day worse"
(let [{:strs [test-client-id]} (setup-test-data [])]
(enable-service-charges! test-client-id)
@(dc/transact conn [(order test-client-id "ezcater-sc"
{:sales-order/external-id "ezcater/order/TEST-ezcater-sc"
:sales-order/vendor :vendor/ccp-ezcater
:sales-order/service-charge -75.0})])
(is (nil? (service-charges-for test-client-id))))))
(deftest service-charges-recognise-square-orders-that-carry-no-vendor
(testing "whole eras of Square orders have no :sales-order/vendor at all; a gate on vendor
alone would silently credit nothing"
(let [{:strs [test-client-id]} (setup-test-data [])]
(enable-service-charges! test-client-id)
@(dc/transact conn [(order test-client-id "vendorless" {:sales-order/service-charge 12.5})])
(is (= 12.5 (:ledger-mapped/amount (service-charges-for test-client-id)))))))
(deftest service-charges-ignore-vendorless-orders-from-other-sources
(testing "the external id fallback is Square-specific, not a catch-all for missing vendors"
(let [{:strs [test-client-id]} (setup-test-data [])]
(enable-service-charges! test-client-id)
@(dc/transact conn [(order test-client-id "ezcater-vendorless"
{:sales-order/external-id "ezcater/order/TEST-ezcater-vendorless"
:sales-order/service-charge -75.0})])
(is (nil? (service-charges-for test-client-id))))))
(defn- refund
"A card refund on `sales-date`. The client+date tuple is set explicitly because
`scan-sales-refunds` walks that index rather than the plain attributes."
[client id total]
{:db/id (str "refund-" id)
:sales-refund/external-id (str "square/refund/TEST-" id)
:sales-refund/client client
:sales-refund/date sales-date
:sales-refund/client+date [client sales-date]
:sales-refund/type "CARD"
:sales-refund/total total})
(defn- enable-refund-only-returns! [client]
@(dc/transact conn [{:db/id client
:client/feature-flags [sut/refund-only-returns-flag]}]))
(defn- returns-for [client]
(#'sut/get-returns client sales-date))
(deftest refund-only-day-needs-the-feature-flag
(testing "without the flag a day of refunds and no sales books no return, as it does today"
(let [{:strs [test-client-id]} (setup-test-data [])]
@(dc/transact conn [(refund test-client-id "unflagged" 40.0)])
(is (nil? (returns-for test-client-id))))))
(deftest refund-only-day-books-a-return_and_balances
(testing "a refund credited on a day with no sales has nothing to offset it, so the day is out
by the refunded amount until a return is recognised against it"
(let [{:strs [test-client-id]} (setup-test-data [])]
(enable-refund-only-returns! test-client-id)
@(dc/transact conn [(refund test-client-id "orphan-a" 30.0)
(refund test-client-id "orphan-b" 10.0)])
(let [returns (returns-for test-client-id)]
(is (= 40.0 (:ledger-mapped/amount returns)))
(is (= :ledger-side/debit (:ledger-mapped/ledger-side returns)))
(is (= 0.0 (d-ss/imbalance (cons returns (sut/get-refund-items test-client-id sales-date))))
"the refund credits and the return debit cancel exactly")))))
(deftest a-day-that-traded-keeps-its-order-derived-return
(testing "the guard is what makes this safe: a day with sales is left entirely alone, so no
trading day can have its return moved by an unmatched refund"
(let [{:strs [test-client-id]} (setup-test-data [])]
(enable-refund-only-returns! test-client-id)
@(dc/transact conn [(order test-client-id "traded" {:sales-order/returns 7.0})
(refund test-client-id "same-day" 40.0)])
(is (= 7.0 (:ledger-mapped/amount (returns-for test-client-id)))
"the order's own return, not the refunded total"))))
(deftest a-day-that-traded-and-returned-nothing-books-no-return
(testing "sales with no returns must not pick up the refunded total either — the guard is on
whether the client traded, not on whether the order-derived figure happened to be nil"
(let [{:strs [test-client-id]} (setup-test-data [])]
(enable-refund-only-returns! test-client-id)
@(dc/transact conn [(order test-client-id "traded-no-returns" {})
(refund test-client-id "unmatched" 40.0)])
(is (nil? (returns-for test-client-id))))))
(deftest dirty-summaries-stop-at-the-client-boundary
(testing "every dirty day for the client is returned, and none belonging to another client.
:sales-summary/client+dirty sorts by client, so an unbounded index scan would walk
every later client's summaries too — correct, but quadratic in the summary count."
(let [{:strs [test-client-id]} (setup-test-data [])
other (get-in @(dc/transact conn [{:db/id "other" :client/code (str "OTHER" (rand-int 100000))}])
[:tempids "other"])
day (fn [client d dirty?]
{:sales-summary/client client
:sales-summary/date d
:sales-summary/dirty dirty?})]
@(dc/transact conn [(day test-client-id #inst "2026-08-01T07:00:00.000-00:00" true)
(day test-client-id #inst "2026-08-02T07:00:00.000-00:00" true)
(day test-client-id #inst "2026-08-03T07:00:00.000-00:00" false)
(day other #inst "2026-08-01T07:00:00.000-00:00" true)
(day other #inst "2026-08-02T07:00:00.000-00:00" true)])
(let [mine (sut/dirty-sales-summaries test-client-id)]
(is (= 2 (count mine)) "both dirty days, and not the clean one")
(is (every? #(= test-client-id (:db/id (:sales-summary/client %))) mine)
"and nothing belonging to the other client"))
(is (= 2 (count (sut/dirty-sales-summaries other)))
"the other client's own dirty days are still found"))))

View File

@@ -70,3 +70,24 @@
(is (= "NICK THE GREEK" (:customer-identifier result)))
(is (= "600 VISTA WAY" (str/trim (:account-number result))))
(is (= "946.24" (:total result)))))))
(deftest parse-reel-produce-statement-28676
(testing "Should parse the Reel Produce statement layout that no longer prints 'Reel Produce' on the page"
(let [pdf-file (io/file "dev-resources/Statement1_from_REEL_Produce_Inc.28676.pdf")
pdf-text (:out (clojure.java.shell/sh "pdftotext" "-layout" (str pdf-file) "-"))
results (sut/parse pdf-text)]
(is (seq results) "Template should match and return results")
(is (= 7 (count results)) "Should parse 7 invoices from statement")
(doseq [result results]
(is (= "Reel Produce" (:vendor-code result)))
(is (= "Sushi Confidential - San Jose" (:customer-identifier result))))
(is (= ["454379" "454826" "455120" "455683" "456654" "456774" "457171"]
(mapv :invoice-number results)))
(is (= ["1003.10" "530.85" "605.00" "1187.40" "164.00" "675.60" "265.75"]
(mapv :total results)))
;; totals add up to the statement's $4,431.70 amount due
(is (= 4431.70 (->> results (map #(Double/parseDouble (:total %))) (reduce +))))
(let [d (:date (first results))]
(is (= 2026 (time/year d)))
(is (= 6 (time/month d)))
(is (= 23 (time/day d)))))))

View File

@@ -0,0 +1,118 @@
(ns auto-ap.square.core3-test
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.integration.util :refer [setup-test-data wrap-setup]]
[auto-ap.square.core3 :as sut]
[clojure.test :refer [deftest is testing use-fixtures]]
[datomic.api :as dc]))
(use-fixtures :each wrap-setup)
(def client {:client/code "NGCD"})
(def location {:square-location/client-location "CD"})
(defn- refund-count []
(count (dc/q '[:find ?e :where [?e :sales-refund/external-id]] (dc/db conn))))
(defn- resolve-refund [id]
(sut/existing-id (dc/db conn) :sales-refund/external-id "square/refund/" client location id))
(deftest scoped-key-carries-client-and-location
(testing "the same shape sales order keys already use, so a shared location cannot contend"
(is (= "square/refund/NGCD-CD-abc" (sut/scoped-key "square/refund/" client location "abc")))
(is (= "square/charge/NGCD-CD-xyz" (sut/scoped-key "square/charge/" client location "xyz")))))
(deftest legacy-keyed-entity-is-updated-not-duplicated
(testing "an entity still carrying its unscoped key is found and re-keyed in place.
This is the sharpest hazard in the migration: these external ids are
:db.unique/identity, so writing the new key without resolving the old one first
matches nothing and creates a second entity, orphaning the original."
(setup-test-data [])
@(dc/transact conn [{:db/id "r"
:sales-refund/external-id "square/refund/abc"
:sales-refund/total 10.0}])
(is (= 1 (refund-count)))
(let [eid (resolve-refund "abc")]
(is (some? eid) "resolves an entity carrying the legacy key")
@(dc/transact conn [{:db/id eid
:sales-refund/external-id (sut/scoped-key "square/refund/" client location "abc")
:sales-refund/total 10.0}])
(is (= 1 (refund-count)) "no second entity was created")
(is (= eid (dc/entid (dc/db conn) [:sales-refund/external-id "square/refund/NGCD-CD-abc"]))
"the same entity now answers to the scoped key")
(is (nil? (dc/entid (dc/db conn) [:sales-refund/external-id "square/refund/abc"]))
"and no longer to the legacy one"))))
(deftest already-scoped-entity-resolves-by-its-new-key
(testing "re-running the importer after migration finds the entity by the scoped key, so the
migration is not undone and nothing is duplicated"
(setup-test-data [])
@(dc/transact conn [{:db/id "r"
:sales-refund/external-id "square/refund/NGCD-CD-abc"
:sales-refund/total 10.0}])
(is (= (dc/entid (dc/db conn) [:sales-refund/external-id "square/refund/NGCD-CD-abc"])
(resolve-refund "abc")))
(is (= 1 (refund-count)))))
(deftest unknown-id-resolves-to-nothing
(testing "a refund never seen before has no id to pin, so the importer creates it fresh"
(setup-test-data [])
(is (nil? (resolve-refund "never-seen")))))
(deftest two-clients-on-one-location-get-their-own-entities
(testing "the point of the re-key: with the client in the key, a second client importing the
same Square refund creates its own entity instead of taking ownership of the first"
(setup-test-data [])
(let [other {:client/code "NGCC"}
other-loc {:square-location/client-location "CC"}]
@(dc/transact conn [{:db/id "r"
:sales-refund/external-id (sut/scoped-key "square/refund/" client location "shared")
:sales-refund/total 10.0}])
(is (nil? (sut/existing-id (dc/db conn) :sales-refund/external-id "square/refund/" other other-loc "shared"))
"the second client does not resolve onto the first client's entity")
@(dc/transact conn [{:db/id "r2"
:sales-refund/external-id (sut/scoped-key "square/refund/" other other-loc "shared")
:sales-refund/total 10.0}])
(is (= 2 (refund-count)) "two stable entities, one per client, rather than one that flips"))))
(deftest payouts-and-shifts-are-client-scoped-too
(testing "expected deposits and cash drawer shifts are fetched per location, so two clients on
one location collide on them exactly as refunds and charges did"
(is (= "square/payout/NGCD-CD-po1"
(sut/scoped-key "square/payout/" client location "po1")))
(is (= "square/cash-drawer-shift/NGCD-CD-sh1"
(sut/scoped-key "square/cash-drawer-shift/" client location "sh1")))))
(deftest legacy-keyed-deposit-is-updated-not-duplicated
(testing "a payout still carrying its unscoped key is found and re-keyed in place"
(setup-test-data [])
@(dc/transact conn [{:db/id "d"
:expected-deposit/external-id "square/payout/po1"
:expected-deposit/total 100.0}])
(let [eid (sut/existing-id (dc/db conn) :expected-deposit/external-id "square/payout/" client location "po1")]
(is (some? eid) "resolves the entity carrying the legacy key")
@(dc/transact conn [{:db/id eid
:expected-deposit/external-id (sut/scoped-key "square/payout/" client location "po1")
:expected-deposit/total 100.0}])
(is (= 1 (count (dc/q '[:find ?e :where [?e :expected-deposit/external-id]] (dc/db conn))))
"no second deposit was created")
(is (nil? (dc/entid (dc/db conn) [:expected-deposit/external-id "square/payout/po1"]))
"the legacy key is gone"))))
(deftest two-clients-get-their-own-deposit
(testing "with the client in the key, a second client importing the same Square payout creates
its own entity instead of taking ownership of the first"
(setup-test-data [])
(let [other {:client/code "NGCC"}
other-loc {:square-location/client-location "CC"}]
@(dc/transact conn [{:db/id "d"
:expected-deposit/external-id (sut/scoped-key "square/payout/" client location "shared")
:expected-deposit/total 100.0}])
(is (nil? (sut/existing-id (dc/db conn) :expected-deposit/external-id "square/payout/" other other-loc "shared"))
"the second client does not resolve onto the first client's deposit")
@(dc/transact conn [{:db/id "d2"
:expected-deposit/external-id (sut/scoped-key "square/payout/" other other-loc "shared")
:expected-deposit/total 100.0}])
(is (= 2 (count (dc/q '[:find ?e :where [?e :expected-deposit/external-id]] (dc/db conn))))
"two stable entities, one per client, rather than one that flips"))))

View File

@@ -0,0 +1,153 @@
(ns auto-ap.tools.compare-sales-summaries
"Compares sales summaries between two points in the same database.
A verification tool, not part of the running application: it lives on the test/dev classpath so
nothing in production can depend on it. Load it from a REPL when auditing a recompute.
The question this exists to answer is narrower than \"did the totals improve\": it is *which
days changed, and were any of them already balanced*. A day that was balanced before and still
balances after can still have had its line amounts move, and that is a real change to the
books even though no red turns green. Counting only balanced/unbalanced transitions would hide
it entirely.
DO NOT USE `compare-against` — OR ANY `d/as-of` DATABASE — TO COMPARE AMOUNTS. The obvious
reading is that Datomic keeps every past value, so a recompute can be audited against what was
there before with no snapshot. That does not hold here: `:ledger-mapped/amount`,
`:ledger-mapped/ledger-side` and `:ledger-mapped/account` are all `:db/noHistory true`, so
superseded values are discarded rather than retained. A summary that has since been recomputed
reads back through `as-of` with its categories intact and its amounts *absent* — which is
indistinguishable from a legitimate all-zero day, and quietly turns every rewritten summary
into a false \"was balanced, still balances\".
To compare amounts, capture `summaries-in` from a live `(d/db conn)` immediately after each
run, keep the two captures outside the database, and diff those. `compare-window` is safe when
both arguments are live database values; only the historical read is unsound. Categories and
which-days-changed do survive `as-of`, since `:sales-summary-item/category` retains history."
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.datomic.sales-summaries :as d-ss]
[clj-time.coerce :as c]
[datomic.api :as dc]))
(def item-read
[:sales-summary-item/category
:sales-summary-item/manual?
:ledger-mapped/amount
{:ledger-mapped/ledger-side [:db/ident]}
{:ledger-mapped/account [:account/numeric-code]}])
(defn- cents
"Amounts are doubles carrying float noise, so compare them at the cent — the unit the books are
actually kept in. Without this, 182.87000000000003 and 182.87 read as a change."
[x]
(Math/round (* 100.0 (double (or x 0.0)))))
(defn- line
"One item reduced to what a reader would call \"the number\": category, side, amount, account."
[item]
{:category (:sales-summary-item/category item)
:side (get-in item [:ledger-mapped/ledger-side :db/ident])
:cents (cents (:ledger-mapped/amount item))
:account (get-in item [:ledger-mapped/account :account/numeric-code])})
(defn summaries-in
"`{[client-code date] {:lines … :imbalance … :balanced?}}` for every summary in `[start end)`.
Keyed by client code and date rather than entity id so the two sides line up even if an entity
were recreated between the points being compared."
[db start end]
(->> (dc/q {:find [(list 'pull '?s [:sales-summary/date
{:sales-summary/client [:client/code]}
{:sales-summary/items item-read}])]
:in '[$ ?start ?end]
:where '[[?s :sales-summary/date ?d]
[(>= ?d ?start)]
[(< ?d ?end)]]}
db (c/to-date start) (c/to-date end))
(map first)
(reduce (fn [acc s]
(let [items (map d-ss/<-pulled-item (:sales-summary/items s))]
(assoc acc
[(get-in s [:sales-summary/client :client/code]) (:sales-summary/date s)]
{:lines (frequencies (map line (:sales-summary/items s)))
:imbalance (d-ss/imbalance items)
:balanced? (d-ss/balanced? items)})))
{})))
(defn- classify
"How one client-day differs. `:numbers-changed` is the interesting one — the lines themselves
moved, whether or not the day's balance status did."
[before after]
(cond
(nil? before) :added
(nil? after) :removed
(= (:lines before) (:lines after)) :identical
:else :numbers-changed))
(defn compare-window
"Compares every summary in `[start end)` between two database values.
Returns per-day rows plus the tallies worth reporting, including the one that is easy to miss:
days that were **already balanced** and whose numbers moved anyway."
[before-db after-db start end]
(let [before (summaries-in before-db start end)
after (summaries-in after-db start end)
rows (for [k (distinct (concat (keys before) (keys after)))
:let [b (get before k) a (get after k)]]
{:client (first k)
:date (second k)
:change (classify b a)
:was-balanced? (:balanced? b)
:now-balanced? (:balanced? a)
:before-imbalance (:imbalance b)
:after-imbalance (:imbalance a)
:lines-before (:lines b)
:lines-after (:lines a)})
rows (vec rows)
changed (filter #(= :numbers-changed (:change %)) rows)]
{:rows rows
:tally {:compared (count rows)
:identical (count (filter #(= :identical (:change %)) rows))
:numbers-changed (count changed)
:added (count (filter #(= :added (:change %)) rows))
:removed (count (filter #(= :removed (:change %)) rows))}
:balance-transitions
{:unbalanced->balanced (count (filter #(and (false? (:was-balanced? %)) (true? (:now-balanced? %))) rows))
:balanced->unbalanced (count (filter #(and (true? (:was-balanced? %)) (false? (:now-balanced? %))) rows))
:stayed-balanced (count (filter #(and (true? (:was-balanced? %)) (true? (:now-balanced? %))) rows))
:stayed-unbalanced (count (filter #(and (false? (:was-balanced? %)) (false? (:now-balanced? %))) rows))}
:previously-balanced-and-changed
(->> changed (filter :was-balanced?) vec)}))
(defn line-diff
"Which categories actually moved on one row, as `{category [before-cents after-cents]}`. For
reading a handful of rows by hand once the tallies point at them."
[row]
(let [by-cat (fn [lines] (reduce (fn [m [l n]] (assoc m (:category l) (* n (:cents l)))) {} lines))
b (by-cat (:lines-before row))
a (by-cat (:lines-after row))]
(->> (distinct (concat (keys b) (keys a)))
(keep (fn [cat]
(let [x (get b cat 0) y (get a cat 0)]
(when (not= x y) [cat [(/ x 100.0) (/ y 100.0)]]))))
(into {}))))
(defn compare-against
"Compare the current database against its own past value at basis `t`.
UNSOUND FOR AMOUNTS — see the namespace docstring. The amount, side and account attributes are
`:db/noHistory`, so any summary rewritten since `t` reads back with no amounts and appears
balanced. Kept only for comparing categories and identifying which days changed."
[t start end]
(let [db (dc/db conn)]
(compare-window (dc/as-of db t) db start end)))
(comment
;; the restore point, i.e. production's own summaries before any of this work
(def result (compare-against 209608347
(clj-time.core/date-time 2026 7 15)
(clj-time.core/date-time 2026 8 14)))
(:tally result)
(:balance-transitions result)
(count (:previously-balanced-and-changed result))
(map line-diff (take 3 (:previously-balanced-and-changed result))))