Compare commits
20 Commits
integreat-
...
7155e199e8
| Author | SHA1 | Date | |
|---|---|---|---|
| 7155e199e8 | |||
| 2b5fbaca00 | |||
| 473556a45d | |||
| 604d1ee1cf | |||
| e095cb94e4 | |||
| 9d007ee8e2 | |||
| fa25620b7a | |||
| d012283362 | |||
| 14726d0208 | |||
| 6429cf823d | |||
| 111eca413e | |||
| 80e45c026e | |||
| 9cd3d9c962 | |||
| 5f5686c39c | |||
| d0846f91aa | |||
| 56addf8c70 | |||
| ebb48a0de4 | |||
| fc54b92ddb | |||
| 019a1b4cd8 | |||
| 38575aa5bd |
@@ -173,4 +173,4 @@
|
||||
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"}}
|
||||
:plaid {:base-url "https://production.plaid.com"
|
||||
:client-id "61bfab05f7e762001b323f79"
|
||||
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}}
|
||||
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}}
|
||||
|
||||
@@ -31,5 +31,5 @@
|
||||
:yodlee2-proxy-port 8888
|
||||
:plaid {:base-url "https://production.plaid.com"
|
||||
:client-id "61bfab05f7e762001b323f79"
|
||||
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}
|
||||
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}
|
||||
}
|
||||
|
||||
@@ -34,5 +34,5 @@
|
||||
:yodlee2-proxy-port 8888
|
||||
:plaid {:base-url "https://production.plaid.com"
|
||||
:client-id "61bfab05f7e762001b323f79"
|
||||
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}
|
||||
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}
|
||||
}
|
||||
|
||||
@@ -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,12 +25,12 @@
|
||||
: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"}}
|
||||
:client-id "61bfab05f7e762001b323f79"
|
||||
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}}
|
||||
|
||||
@@ -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"}
|
||||
}
|
||||
|
||||
BIN
dev-resources/Statement1_from_REEL_Produce_Inc.28676.pdf
Normal file
BIN
dev-resources/Statement1_from_REEL_Produce_Inc.28676.pdf
Normal file
Binary file not shown.
1
resources/.~lock.sample-yodlee-manual.tsv#
Normal file
1
resources/.~lock.sample-yodlee-manual.tsv#
Normal file
@@ -0,0 +1 @@
|
||||
,noti,pop-os,01.06.2026 21:02,file:///home/noti/.config/libreoffice/4;
|
||||
@@ -268,6 +268,21 @@ const calendarYearPeriod = (date) => {
|
||||
return {end: formatDateMMDDYYYY(end), start: formatDateMMDDYYYY(start)};
|
||||
}
|
||||
|
||||
const previousCalendarYearPeriod = (date) => {
|
||||
if (!date) {
|
||||
date= new Date()
|
||||
} else {
|
||||
date = parseMMDDYYYY(date)
|
||||
}
|
||||
const priorYear = date.getFullYear() - 1;
|
||||
|
||||
// Jan 1 - Dec 31 of the previous calendar year
|
||||
const start = new Date(priorYear, 0, 1);
|
||||
const end = new Date(priorYear, 11, 31);
|
||||
|
||||
return {end: formatDateMMDDYYYY(end), start: formatDateMMDDYYYY(start)};
|
||||
}
|
||||
|
||||
const getLastMonthPeriods = (date) => {
|
||||
if (!date) {
|
||||
date = new Date();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1759,7 +1759,7 @@ 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,
|
||||
|
||||
|
43
resources/sysco_recode/bad.csv
Normal file
43
resources/sysco_recode/bad.csv
Normal 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,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
271
scratch-sessions/sysco_recode_scratch.clj
Normal file
271
scratch-sessions/sysco_recode_scratch.clj
Normal 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)))
|
||||
375
src/clj/auto_ap/jobs/backfill_olo_processors.clj
Normal file
375
src/clj/auto_ap/jobs/backfill_olo_processors.clj
Normal 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.
|
||||
)
|
||||
@@ -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]+)"
|
||||
|
||||
@@ -281,15 +281,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
|
||||
|
||||
|
||||
@@ -55,9 +55,9 @@
|
||||
[:div.flex.flex-col.gap-2
|
||||
(buttons/a-button- {"@click" "periods=getFourWeekPeriodsPeriods(source_date)"} [:span "13 periods, ending "
|
||||
[:span {:x-text "source_date"}]])
|
||||
(buttons/a-button- {"@click" "periods=[calendarYearPeriod(source_date)]"} [:span "Calendar year ("
|
||||
[:span {:x-text "parseMMDDYYYY(source_date).getFullYear()"}]
|
||||
")"])
|
||||
(buttons/a-button- {"@click" "periods=[previousCalendarYearPeriod(source_date)]"} [:span "Previous Calendar Year ("
|
||||
[:span {:x-text "parseMMDDYYYY(source_date).getFullYear() - 1"}]
|
||||
")"])
|
||||
(buttons/a-button- {"@click" "periods=getTwelveCalendarMonthsPeriods(source_date)"} [:span "12 months, ending "
|
||||
[:span {:x-text "parseMMDDYYYY(source_date).toLocaleString('default', { month: 'long' })"}]])
|
||||
[:hr {:class "h-px my-1 bg-gray-200 border-0 dark:bg-gray-700"}]
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
{:exact-match-id (:db/id p)})
|
||||
:content (str (format "$%,.2f" (:payment/amount p))
|
||||
(some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}]
|
||||
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page)
|
||||
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/page)
|
||||
{:exact-match-id (:db/id (first (:payment/transaction p)))})
|
||||
:color :secondary
|
||||
:content "Transaction"})))))
|
||||
|
||||
@@ -109,6 +109,33 @@
|
||||
:placeholder "e.g., ABC-456"
|
||||
:size :small}))
|
||||
|
||||
(when (:external? (:route-params request))
|
||||
(com/field {:label "Source"}
|
||||
(com/text-input {:name "source"
|
||||
:id "source"
|
||||
:class "hot-filter"
|
||||
:value (:source (:query-params request))
|
||||
:placeholder "e.g., invoice"
|
||||
:size :small})))
|
||||
|
||||
(when (:external? (:route-params request))
|
||||
(com/field {:label "External Id"}
|
||||
(com/text-input {:name "external-id-like"
|
||||
:id "external-id-like"
|
||||
:class "hot-filter"
|
||||
:value (:external-id-like (:query-params request))
|
||||
:placeholder "e.g., ABC-123"
|
||||
:size :small})))
|
||||
|
||||
(when (:external? (:route-params request))
|
||||
(com/field {:label "Location"}
|
||||
(com/text-input {:name "location"
|
||||
:id "location"
|
||||
:class "hot-filter"
|
||||
:value (:location (:query-params request))
|
||||
:placeholder "SC"
|
||||
:size :small})))
|
||||
|
||||
(com/field {:label "Account Code"}
|
||||
[:div.flex.space-x-4.items-baseline
|
||||
(com/int-input {:name "numeric-code-gte"
|
||||
@@ -455,6 +482,9 @@
|
||||
[:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/name]}]]]
|
||||
[:check-number {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:invoice-number {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:source {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:external-id-like {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:location {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:status {:optional true} [:maybe (ref->enum-schema "invoice-status")]]
|
||||
[:exact-match-id {:optional true} [:maybe entity-id]]
|
||||
[:all-selected {:optional true :default nil} [:maybe :boolean]]
|
||||
@@ -528,7 +558,9 @@
|
||||
:title (fn [r]
|
||||
(str
|
||||
(some-> r :route-params :status name str/capitalize (str " "))
|
||||
"Register"))
|
||||
(if (:external? (:route-params r))
|
||||
"External Register"
|
||||
"Register")))
|
||||
:entity-name "register"
|
||||
:route ::route/table
|
||||
:csv-route ::route/csv
|
||||
@@ -629,14 +661,15 @@
|
||||
:color :primary
|
||||
:content (format "Invoice '%s'" (-> i :journal-entry/original-entity :invoice/invoice-number))})
|
||||
(-> i :journal-entry/original-entity :invoice/source-url)
|
||||
{:link (-> i :journal-entry/original-entity :invoice/source-url)
|
||||
:color :secondary
|
||||
:content (str "File")}
|
||||
(conj
|
||||
{:link (-> i :journal-entry/original-entity :invoice/source-url)
|
||||
:color :secondary
|
||||
:content (str "File")})
|
||||
|
||||
(-> i :journal-entry/original-entity :transaction/description-original)
|
||||
(conj
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::transaction-routes/all-page)
|
||||
::transaction-routes/page)
|
||||
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
||||
:color :primary
|
||||
:content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))})
|
||||
|
||||
@@ -423,7 +423,7 @@
|
||||
{:exact-match-id (:db/id invoice)})
|
||||
:content (str "Inv. " (:invoice/invoice-number invoice))})))
|
||||
(some-> p :transaction/_payment ((fn [t]
|
||||
[{:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page)
|
||||
[{:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/page)
|
||||
{:exact-match-id (:db/id (first t))})
|
||||
:color :secondary
|
||||
:content "Transaction"}]))))))}]}))
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
svg/external-link))
|
||||
(when-let [transaction-id (-> e (:transaction/_expected-deposit) first :db/id)]
|
||||
(com/a-button {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
::transaction-routes/all-page)
|
||||
::transaction-routes/page)
|
||||
"?exact-match-id="
|
||||
transaction-id)} "Transaction"))])
|
||||
:headers [{:key "client"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [apply-pagination apply-sort-3 conn merge-query pull-many
|
||||
query2]]
|
||||
pull-many-by-id query2]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
@@ -128,6 +128,38 @@
|
||||
(map #(:ledger-mapped/amount % 0.0))
|
||||
(reduce + 0.0)))
|
||||
|
||||
(def csv-account-read
|
||||
[:db/id
|
||||
:account/name
|
||||
:account/code
|
||||
{:account/client-overrides [:account-client-override/name
|
||||
{:account-client-override/client [:db/id]}]}])
|
||||
|
||||
(defn- csv-amount
|
||||
"Renders an item's amount when it falls on `side`, fixed to cents so the CSV
|
||||
never carries floating point noise."
|
||||
[side item]
|
||||
(when (= side (:ledger-mapped/ledger-side item))
|
||||
(format "%.2f" (double (:ledger-mapped/amount item 0.0)))))
|
||||
|
||||
(defn summaries->csv-rows
|
||||
"Flattens each summary into one row per item, so every category is its own
|
||||
auditable CSV line. Accounts are resolved in a single batch pull and
|
||||
clientized against the summary's own client."
|
||||
[summaries]
|
||||
(let [account-ids (into #{}
|
||||
(comp (mapcat :sales-summary/items)
|
||||
(keep (comp :db/id :ledger-mapped/account)))
|
||||
summaries)
|
||||
id->account (if (seq account-ids)
|
||||
(pull-many-by-id (dc/db conn) csv-account-read account-ids)
|
||||
{})]
|
||||
(for [ss summaries
|
||||
item (sort-items (:sales-summary/items ss))]
|
||||
(assoc (merge item ss)
|
||||
::account (some-> item :ledger-mapped/account :db/id id->account
|
||||
(d-accounts/clientize (-> ss :sales-summary/client :db/id)))))))
|
||||
|
||||
(defn truncate [s max-len]
|
||||
(if (> (count s) max-len)
|
||||
(str (subs s 0 (- max-len 3)) "...")
|
||||
@@ -215,7 +247,15 @@
|
||||
:title "Sales Summaries"
|
||||
:entity-name "Daily Summary"
|
||||
:route ::route/table
|
||||
:headers [{:key "client"
|
||||
:csv-route ::route/csv
|
||||
:page->csv-entities (fn [[summaries]]
|
||||
(summaries->csv-rows summaries))
|
||||
:headers [{:key "id"
|
||||
:name "Id"
|
||||
:render-csv :db/id
|
||||
:render-for #{:csv}}
|
||||
|
||||
{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [args]
|
||||
@@ -231,6 +271,7 @@
|
||||
:name "Debits"
|
||||
:sort-key "debits"
|
||||
:class "w-72 align-top"
|
||||
:render-for #{:html}
|
||||
:render (fn [ss]
|
||||
(let [items (:sales-summary/items ss)
|
||||
debit-items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)) (sort-items items))
|
||||
@@ -257,6 +298,7 @@
|
||||
:name "Credits"
|
||||
:sort-key "credits"
|
||||
:class "w-72 align-top"
|
||||
:render-for #{:html}
|
||||
:render (fn [ss]
|
||||
(let [items (:sales-summary/items ss)
|
||||
credit-items (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)) (sort-items items))
|
||||
@@ -283,6 +325,7 @@
|
||||
:name "Status"
|
||||
:sort-key "balance"
|
||||
:class "w-28 align-top"
|
||||
:render-for #{:html}
|
||||
:render (fn [ss]
|
||||
(let [items (:sales-summary/items ss)
|
||||
total-debits (total-debits items)
|
||||
@@ -304,10 +347,41 @@
|
||||
[:span.text-xs.uppercase.tracking-wider.text-red-600.font-medium.mt-0.5
|
||||
(if (> total-debits total-credits) "Debit over" "Credit over")]])]))}
|
||||
|
||||
{:key "category"
|
||||
:name "Category"
|
||||
:render-csv :sales-summary-item/category
|
||||
:render-for #{:csv}}
|
||||
|
||||
{:key "account-code"
|
||||
:name "Account Code"
|
||||
:render-csv #(-> % ::account :account/code)
|
||||
:render-for #{:csv}}
|
||||
|
||||
{:key "account"
|
||||
:name "Account"
|
||||
:render-csv #(-> % ::account :account/name)
|
||||
:render-for #{:csv}}
|
||||
|
||||
{:key "debit"
|
||||
:name "Debit"
|
||||
:render-csv (partial csv-amount :ledger-side/debit)
|
||||
:render-for #{:csv}}
|
||||
|
||||
{:key "credit"
|
||||
:name "Credit"
|
||||
:render-csv (partial csv-amount :ledger-side/credit)
|
||||
:render-for #{:csv}}
|
||||
|
||||
{:key "manual"
|
||||
:name "Manual"
|
||||
:render-csv #(boolean (:sales-summary-item/manual? %))
|
||||
:render-for #{:csv}}
|
||||
|
||||
{:key "links"
|
||||
:name "Links"
|
||||
:show-starting "lg"
|
||||
:class "w-8"
|
||||
:render-for #{:html}
|
||||
:render (fn [ss]
|
||||
(let [ledger-entry (:journal-entry/original-entity ss)]
|
||||
(when (seq ledger-entry)
|
||||
@@ -754,6 +828,7 @@
|
||||
(->>
|
||||
{::route/page (helper/page-route grid-page)
|
||||
::route/table (helper/table-route grid-page)
|
||||
::route/csv (helper/csv-route grid-page)
|
||||
::route/edit-wizard (-> mm/open-wizard-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-init-multi-form-state initial-edit-wizard-state)
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
(def transaction-approval-status
|
||||
{:transaction-approval-status/unapproved "Unapproved"
|
||||
:transaction-approval-status/approved "Approved"
|
||||
:transaction-approval-status/suppressed "Client Review"})
|
||||
:transaction-approval-status/requires-feedback "Client Review"})
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
|
||||
@@ -988,8 +988,8 @@
|
||||
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'unapproved' }"
|
||||
:class "rounded-r-lg"}
|
||||
"Unapproved")
|
||||
(com/button-group-button {"@click" "approvalStatus = 'suppressed'"
|
||||
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'suppressed' }"
|
||||
(com/button-group-button {"@click" "approvalStatus = 'requires-feedback'"
|
||||
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'requires-feedback' }"
|
||||
:class "rounded-r-lg"}
|
||||
"Client Review")]])))]]]])
|
||||
:footer
|
||||
@@ -1049,7 +1049,7 @@
|
||||
|
||||
(when-not (dollars= (- (:transaction/amount transaction))
|
||||
(:payment/amount payment))
|
||||
(throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"})))
|
||||
(form-validation-error "Amounts don't match"))
|
||||
(if (is-already-linked-to-this-payment? transaction payment-id)
|
||||
(save-memo-only request)
|
||||
(save-linked-transaction request payment))
|
||||
@@ -1077,16 +1077,16 @@
|
||||
(exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction)))
|
||||
|
||||
(when (:transaction/payment transaction)
|
||||
(throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"})))
|
||||
(form-validation-error "Transaction already linked"))
|
||||
|
||||
(when (or (> (count invoice-clients) 1)
|
||||
(not= (-> transaction :transaction/client :db/id)
|
||||
(first invoice-clients)))
|
||||
(throw (ex-info "Clients don't match" {:validation-error "Invoice(s) and transaction client do not match."})))
|
||||
(form-validation-error "Invoice(s) and transaction client do not match."))
|
||||
|
||||
(when-not (dollars= (- (:transaction/amount transaction))
|
||||
invoice-amount)
|
||||
(throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"})))
|
||||
(form-validation-error "Amounts don't match"))
|
||||
|
||||
(let [payment-tx (i-transactions/add-new-payment
|
||||
(dc/pull db [:transaction/amount :transaction/date :db/id] (:db/id transaction))
|
||||
@@ -1122,16 +1122,16 @@
|
||||
(when (or (> (count invoice-clients) 1)
|
||||
(not= (-> transaction :transaction/client :db/id)
|
||||
(first invoice-clients)))
|
||||
(throw (ex-info "Clients don't match" {:validation-error "Invoice(s) and transaction client do not match."
|
||||
:transaction-client (-> transaction :transaction/client :db/id)
|
||||
:invoice-clients invoice-clients})))
|
||||
(form-validation-error "Invoice(s) and transaction client do not match."
|
||||
:transaction-client (-> transaction :transaction/client :db/id)
|
||||
:invoice-clients invoice-clients))
|
||||
|
||||
(when-not (dollars= (- (:transaction/amount transaction))
|
||||
invoice-amount)
|
||||
(throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"})))
|
||||
(form-validation-error "Amounts don't match"))
|
||||
|
||||
(when (:transaction/payment transaction)
|
||||
(throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"})))
|
||||
(form-validation-error "Transaction already linked"))
|
||||
|
||||
(let [payment-tx (i-transactions/add-new-payment
|
||||
(dc/pull db [:transaction/amount :transaction/date :db/id] (:db/id transaction))
|
||||
@@ -1178,12 +1178,10 @@
|
||||
|
||||
(let [description-pattern (some-> transaction-rule :transaction-rule/description iol-ion.query/->pattern)]
|
||||
(when (not (rm/rule-applies? transaction {:transaction-rule/description description-pattern}))
|
||||
(throw (ex-info "Transaction rule does not apply"
|
||||
{:validation-error "Transaction rule does not apply"}))))
|
||||
(form-validation-error "Transaction rule does not apply")))
|
||||
|
||||
(when (:transaction/payment transaction)
|
||||
(throw (ex-info "Transaction already associated with a payment"
|
||||
{:validation-error "Transaction already associated with a payment"})))
|
||||
(form-validation-error "Transaction already associated with a payment"))
|
||||
|
||||
(let [locations (-> transaction :transaction/client :client/locations)
|
||||
updated-tx (rm/apply-rule {:db/id (:db/id transaction)
|
||||
@@ -1336,8 +1334,7 @@
|
||||
(exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction)))
|
||||
|
||||
(when (not= :payment-status/cleared (-> payment :payment/status))
|
||||
(throw (ex-info "Payment can't be undone because it isn't cleared."
|
||||
{:validation-error "Payment can't be undone because it isn't cleared."})))
|
||||
(form-validation-error "Payment can't be undone because it isn't cleared."))
|
||||
|
||||
(let [is-autopay-payment? (some->> (dc/q {:find ['?sp]
|
||||
:in ['$ '?payment]
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
:request request}
|
||||
(com/breadcrumbs {}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
::transaction-routes/all-page)}
|
||||
::transaction-routes/page)}
|
||||
"Transactions"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:transaction-insights)}
|
||||
|
||||
@@ -334,7 +334,7 @@
|
||||
(and (map? data)
|
||||
(every? #(try (Long/parseLong %) true (catch Exception _ false)) (keys data)))
|
||||
(into [] (->> (keys data)
|
||||
sort
|
||||
(sort-by #(Long/parseLong %))
|
||||
(map data)))
|
||||
(nil? data)
|
||||
nil
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
(def routes {"" {:get ::page
|
||||
:put ::edit-wizard-submit}
|
||||
"/table" ::table
|
||||
"/csv" ::csv
|
||||
["/" [#"\d+" :db/id]] {:get ::edit-wizard}
|
||||
"/edit/navigate" ::edit-wizard-navigate
|
||||
"/edit/sales-summary-item" ::new-summary-item
|
||||
|
||||
@@ -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)))))))
|
||||
|
||||
Reference in New Issue
Block a user