initial commit
This commit is contained in:
commit
89b15af73e
12 changed files with 479 additions and 0 deletions
1
README.md
Normal file
1
README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Project repo_PLTo23
|
||||
5
payment-display-p_WxCpgo3/.workflow.state
Normal file
5
payment-display-p_WxCpgo3/.workflow.state
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# DO NOT EDIT THIS FILE
|
||||
---
|
||||
schema: workflow.state/2022.04
|
||||
id: p_WxCpgo3
|
||||
|
||||
34
payment-display-p_WxCpgo3/calculate/entry.js
Normal file
34
payment-display-p_WxCpgo3/calculate/entry.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
export default defineComponent({
|
||||
async run({ steps }) {
|
||||
const rows = steps.parse.$return_value.rows;
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
throw new Error("No parsed rows found from the parse step.");
|
||||
}
|
||||
|
||||
const parseAmount = (value) => {
|
||||
const num = parseFloat(String(value || "0").replace(/,/g, "").trim());
|
||||
return isNaN(num) ? 0 : num;
|
||||
};
|
||||
|
||||
const payments = rows.map((row) => ({
|
||||
method: (row["Method"] || "").trim(),
|
||||
payee: (row["Payee"] || "").trim(),
|
||||
invoice_numbers: (row["Invoice Numbers"] || "").trim(),
|
||||
send_payment_on: (row["Send Payment On"] || "").trim(),
|
||||
amount_usd: parseAmount(row["Amount in USD"]),
|
||||
status: (row["Status"] || "").trim(),
|
||||
company_subsidiary: (row["Company/Subsidiary"] || "").trim(),
|
||||
}));
|
||||
|
||||
const grand_total = payments.reduce((sum, p) => sum + p.amount_usd, 0);
|
||||
|
||||
return {
|
||||
file_name: steps.parse.$return_value.file_name,
|
||||
row_count: payments.length,
|
||||
grand_count: payments.length,
|
||||
grand_total: Number(grand_total.toFixed(2)),
|
||||
payments,
|
||||
};
|
||||
},
|
||||
});
|
||||
61
payment-display-p_WxCpgo3/filter/entry.js
Normal file
61
payment-display-p_WxCpgo3/filter/entry.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
export default {
|
||||
type: "action",
|
||||
props: {
|
||||
_filter: "any",
|
||||
},
|
||||
run({ $ }) {
|
||||
const _filter = this._filter
|
||||
if (!_filter || typeof _filter !== "object") {
|
||||
throw new Error("filter not configured correctly")
|
||||
}
|
||||
const messageOnContinue = "Continuing workflow"
|
||||
const messageOnEnd = "Ending workflow"
|
||||
let result = evalRuleGroup(_filter.rules)
|
||||
const _continue = ((typeof _filter.continue === "undefined") || _filter.continue === "true") ? result : !result
|
||||
return _continue ? $.export("$summary", messageOnContinue) : $.flow.exit(messageOnEnd)
|
||||
},
|
||||
}
|
||||
|
||||
// config.rules
|
||||
function evalRuleGroup(rules) {
|
||||
const and = rules.t === "and"
|
||||
let v = and
|
||||
for (const r of rules.r) {
|
||||
const b = ("t" in r) ? evalRuleGroup(r) : evalRule(r)
|
||||
v = and ? v && b : v || b
|
||||
}
|
||||
return v
|
||||
}
|
||||
// XXX if rule.a is {{something}} should probably NOT +rule.a, but if rule.a is "12" we should... hmm
|
||||
function evalRule(rule) {
|
||||
switch (rule.op) {
|
||||
default: throw new Error(`unhandled op: ${rule.op}`)
|
||||
case "exist": return rule.a !== undefined
|
||||
case "!exist": return rule.a === undefined
|
||||
case "null": return rule.a === null
|
||||
case "!null": return rule.a !== null
|
||||
case "str": return typeof rule.a === "string"
|
||||
case "!str": return typeof rule.a !== "string"
|
||||
case "str ": return typeof rule.a === "string" && !!rule.a.match(/^\s*$/)
|
||||
case "str! ": return typeof rule.a === "string" && !rule.a.match(/^\s*$/)
|
||||
case "str=": return typeof rule.a === "string" && typeof rule.b === "string" && (rule.a.localeCompare(rule.b, undefined, { sensitivity: rule.matchCase ? "variant" : "base" }) == 0)
|
||||
case "str!=": return typeof rule.a === "string" && typeof rule.b === "string" && (rule.a.localeCompare(rule.b, undefined, { sensitivity: rule.matchCase ? "variant" : "base" }) != 0)
|
||||
case "str^": return typeof rule.a === "string" && typeof rule.b === "string" && (rule.matchCase ? rule.a.startsWith(rule.b) : rule.a.toLowerCase().startsWith(rule.b.toLowerCase()))
|
||||
case "str/": return typeof rule.a === "string" && typeof rule.b === "string" && (rule.matchCase ? rule.a.includes(rule.b) : rule.a.toLowerCase().includes(rule.b.toLowerCase()))
|
||||
case "str!/": return typeof rule.a === "string" && typeof rule.b === "string" && (rule.matchCase ? !rule.a.includes(rule.b) : !rule.a.toLowerCase().includes(rule.b.toLowerCase()))
|
||||
case "str$": return typeof rule.a === "string" && typeof rule.b === "string" && (rule.matchCase ? rule.a.endsWith(rule.b) : rule.a.toLowerCase().endsWith(rule.b.toLowerCase()))
|
||||
case "num": return typeof rule.a === "number"
|
||||
case "!num": return typeof rule.a !== "number"
|
||||
case "num=": return typeof +rule.a === "number" && typeof +rule.b === "number" && +rule.a === +rule.b
|
||||
case "num!=": return typeof +rule.a === "number" && typeof +rule.b === "number" && +rule.a !== +rule.b
|
||||
case "num>": return typeof +rule.a === "number" && typeof +rule.b === "number" && +rule.a > +rule.b
|
||||
case "num>=": return typeof +rule.a === "number" && typeof +rule.b === "number" && +rule.a >= +rule.b
|
||||
case "num<": return typeof +rule.a === "number" && typeof +rule.b === "number" && +rule.a < +rule.b
|
||||
case "num<=": return typeof +rule.a === "number" && typeof +rule.b === "number" && +rule.a <= +rule.b
|
||||
case "true": return typeof rule.a === "boolean" && rule.a
|
||||
case "false": return typeof rule.a === "boolean" && !rule.a
|
||||
case "bool": return typeof rule.a === "boolean"
|
||||
case "!bool": return typeof rule.a !== "boolean"
|
||||
case "bool=": return typeof rule.a === "boolean" && typeof rule.b === "boolean" && rule.a === rule.b
|
||||
}
|
||||
}
|
||||
37
payment-display-p_WxCpgo3/parse/entry.js
Normal file
37
payment-display-p_WxCpgo3/parse/entry.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import fs from "fs";
|
||||
import { parse } from "csv-parse/sync";
|
||||
|
||||
export default defineComponent({
|
||||
async run({ steps }) {
|
||||
const filePath = `/tmp/${steps.trigger.event.name}`;
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found at ${filePath}`);
|
||||
}
|
||||
|
||||
const csvText = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
const rows = parse(csvText, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
trim: true,
|
||||
});
|
||||
|
||||
const normalizedRows = rows.map((row) => {
|
||||
const clean = {};
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
clean[String(key).trim()] = typeof value === "string" ? value.trim() : value;
|
||||
}
|
||||
return clean;
|
||||
});
|
||||
|
||||
return {
|
||||
file_path: filePath,
|
||||
file_name: steps.trigger.event.name,
|
||||
row_count: normalizedRows.length,
|
||||
headers: normalizedRows.length ? Object.keys(normalizedRows[0]) : [],
|
||||
sample_rows: normalizedRows.slice(0, 5),
|
||||
rows: normalizedRows,
|
||||
};
|
||||
},
|
||||
});
|
||||
8
payment-display-p_WxCpgo3/prepare_dashboard/entry.js
Normal file
8
payment-display-p_WxCpgo3/prepare_dashboard/entry.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export default defineComponent({
|
||||
async run({ steps }) {
|
||||
return {
|
||||
...steps.calculate.$return_value,
|
||||
last_updated: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
});
|
||||
49
payment-display-p_WxCpgo3/workflow.yaml
Normal file
49
payment-display-p_WxCpgo3/workflow.yaml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
schema: workflow/2022.04
|
||||
name: Payment Display
|
||||
settings:
|
||||
error_notification: true
|
||||
triggers:
|
||||
- id: dc_QyuaX1X
|
||||
steps:
|
||||
- namespace: filter
|
||||
runtime: nodejs20.x
|
||||
uses: "./filter/entry.js"
|
||||
props:
|
||||
_filter:
|
||||
continue: 'true'
|
||||
rules:
|
||||
t: and
|
||||
r:
|
||||
- a: "{{steps.trigger.event.fileExtension}}"
|
||||
op: str=
|
||||
b: "{{steps.trigger.event.fileExtension}}"
|
||||
matchCase: true
|
||||
- namespace: download_file
|
||||
runtime: nodejs20.x
|
||||
uses: google_drive-download-file@0.1.22
|
||||
props:
|
||||
googleDrive:
|
||||
authProvisionId: apn_dbh7Jkb
|
||||
drive: My Drive
|
||||
fileId: "{{steps.trigger.event.id}}"
|
||||
filePath: "/tmp/{{steps.trigger.event.name}}"
|
||||
- namespace: parse
|
||||
runtime: nodejs20.x
|
||||
uses: "./parse/entry.js"
|
||||
- namespace: calculate
|
||||
runtime: nodejs20.x
|
||||
uses: "./calculate/entry.js"
|
||||
- namespace: prepare_dashboard
|
||||
runtime: nodejs20.x
|
||||
uses: "./prepare_dashboard/entry.js"
|
||||
- namespace: save_to_datastore
|
||||
runtime: nodejs20.x
|
||||
uses: data_stores-add-update-multiple-records@0.0.9
|
||||
props:
|
||||
dataStore:
|
||||
storeId: ds_5ludZgA
|
||||
data:
|
||||
current_dashboard: "{{steps.prepare_dashboard.$return_value}}"
|
||||
? "\n"
|
||||
: ''
|
||||
|
||||
5
slack-app-home-p_3nC3laY/.workflow.state
Normal file
5
slack-app-home-p_3nC3laY/.workflow.state
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# DO NOT EDIT THIS FILE
|
||||
---
|
||||
schema: workflow.state/2022.04
|
||||
id: p_3nC3laY
|
||||
|
||||
182
slack-app-home-p_3nC3laY/build_home_view/entry.js
Normal file
182
slack-app-home-p_3nC3laY/build_home_view/entry.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
export default defineComponent({
|
||||
async run({ steps }) {
|
||||
const dashboard = steps.get_current_dashboard.$return_value;
|
||||
const slackEvent = steps.handle_slack_event.$return_value || {};
|
||||
|
||||
const userId =
|
||||
slackEvent.user_id ||
|
||||
slackEvent.raw_body?.event?.user ||
|
||||
slackEvent.raw_body?.user ||
|
||||
slackEvent.raw_body?.authorizations?.[0]?.user_id;
|
||||
|
||||
if (!dashboard || Object.keys(dashboard).length === 0) {
|
||||
throw new Error("No dashboard data found in Data Store.");
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("No Slack user ID found in the event payload.");
|
||||
}
|
||||
|
||||
const payments = Array.isArray(dashboard.payments) ? dashboard.payments : [];
|
||||
|
||||
const formatCurrency = (value) =>
|
||||
new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(Number(value || 0));
|
||||
|
||||
const parseMDYLocal = (value) => {
|
||||
if (!value) return null;
|
||||
const parts = String(value).split("/");
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const [mm, dd, yyyy] = parts.map((p) => parseInt(p, 10));
|
||||
if (!mm || !dd || !yyyy) return null;
|
||||
|
||||
return new Date(yyyy, mm - 1, dd);
|
||||
};
|
||||
|
||||
const formatDateKey = (date) => {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${d}`;
|
||||
};
|
||||
|
||||
const formatDisplayDate = (dateKey) => {
|
||||
const [y, m, d] = dateKey.split("-").map(Number);
|
||||
const dt = new Date(y, m - 1, d);
|
||||
return dt.toLocaleDateString("en-US", {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const futurePayments = payments.filter((p) => {
|
||||
const dt = parseMDYLocal(p.send_payment_on);
|
||||
return dt && dt >= today && (p.method === "ACH" || p.method === "Check");
|
||||
});
|
||||
|
||||
const groupByMethodAndDay = (methodName) => {
|
||||
const grouped = {};
|
||||
|
||||
for (const payment of futurePayments) {
|
||||
if (payment.method !== methodName) continue;
|
||||
|
||||
const dt = parseMDYLocal(payment.send_payment_on);
|
||||
if (!dt) continue;
|
||||
|
||||
const key = formatDateKey(dt);
|
||||
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = {
|
||||
count: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
grouped[key].count += 1;
|
||||
grouped[key].total += payment.amount_usd;
|
||||
}
|
||||
|
||||
return Object.keys(grouped)
|
||||
.sort()
|
||||
.map((dateKey) => ({
|
||||
date_key: dateKey,
|
||||
display_date: formatDisplayDate(dateKey),
|
||||
count: grouped[dateKey].count,
|
||||
total: Number(grouped[dateKey].total.toFixed(2)),
|
||||
}));
|
||||
};
|
||||
|
||||
const achDays = groupByMethodAndDay("ACH");
|
||||
const checkDays = groupByMethodAndDay("Check");
|
||||
|
||||
const buildDayBlocks = (title, days) => {
|
||||
const total = days.reduce((sum, d) => sum + d.total, 0);
|
||||
const count = days.reduce((sum, d) => sum + d.count, 0);
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*${title}*\n${count} payments • ${formatCurrency(total)}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
];
|
||||
|
||||
if (!days.length) {
|
||||
blocks.push({
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: "_No upcoming payments found._",
|
||||
},
|
||||
});
|
||||
blocks.push({ type: "divider" });
|
||||
return blocks;
|
||||
}
|
||||
|
||||
for (const day of days) {
|
||||
blocks.push({
|
||||
type: "section",
|
||||
fields: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*${day.display_date}*`,
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `${day.count} payments • ${formatCurrency(day.total)}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
blocks.push({ type: "divider" });
|
||||
return blocks;
|
||||
};
|
||||
|
||||
return {
|
||||
user_id: userId,
|
||||
view: {
|
||||
type: "home",
|
||||
blocks: [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "Payments Dashboard",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Last updated:* ${dashboard.last_updated || "N/A"}`,
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Source:* ${dashboard.file_name || "N/A"}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
...buildDayBlocks("ACH", achDays),
|
||||
...buildDayBlocks("Check", checkDays),
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
32
slack-app-home-p_3nC3laY/handle_slack_event/entry.js
Normal file
32
slack-app-home-p_3nC3laY/handle_slack_event/entry.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export default defineComponent({
|
||||
async run({ steps, $ }) {
|
||||
const body = steps.trigger.event.body || steps.trigger.event;
|
||||
|
||||
if (body.type === "url_verification") {
|
||||
await $.respond({
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: body.challenge,
|
||||
});
|
||||
|
||||
return { verified: true };
|
||||
}
|
||||
|
||||
await $.respond({
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: { ok: true },
|
||||
});
|
||||
|
||||
return {
|
||||
event_type: body.event?.type,
|
||||
user_id: body.event?.user,
|
||||
tab: body.event?.tab,
|
||||
raw_body: body,
|
||||
};
|
||||
},
|
||||
});
|
||||
34
slack-app-home-p_3nC3laY/publish_home_view/entry.js
Normal file
34
slack-app-home-p_3nC3laY/publish_home_view/entry.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
export default defineComponent({
|
||||
props: {
|
||||
slack_bot_token: {
|
||||
type: "string",
|
||||
label: "Slack Bot Token",
|
||||
secret: true,
|
||||
},
|
||||
},
|
||||
|
||||
async run({ steps }) {
|
||||
const user_id = steps.build_home_view.$return_value.user_id;
|
||||
const view = steps.build_home_view.$return_value.view;
|
||||
|
||||
const res = await fetch("https://slack.com/api/views.publish", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${this.slack_bot_token}`,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id,
|
||||
view,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.ok) {
|
||||
throw new Error(`Slack API error: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
31
slack-app-home-p_3nC3laY/workflow.yaml
Normal file
31
slack-app-home-p_3nC3laY/workflow.yaml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
schema: workflow/2022.04
|
||||
name: Slack App Home
|
||||
settings:
|
||||
error_notification: true
|
||||
triggers:
|
||||
- id: hi_JYH1xQr
|
||||
steps:
|
||||
- namespace: handle_slack_event
|
||||
runtime: nodejs20.x
|
||||
uses: "./handle_slack_event/entry.js"
|
||||
- namespace: get_current_dashboard
|
||||
runtime: nodejs20.x
|
||||
uses: data_stores-get-record-or-create@0.0.15
|
||||
_props_cursor: dyp_97UDZ5q7
|
||||
props:
|
||||
dataStore:
|
||||
storeId: ds_5ludZgA
|
||||
key:
|
||||
__lv:
|
||||
label: current_dashboard
|
||||
value: current_dashboard
|
||||
- namespace: build_home_view
|
||||
runtime: nodejs20.x
|
||||
uses: "./build_home_view/entry.js"
|
||||
- namespace: publish_home_view
|
||||
runtime: nodejs20.x
|
||||
uses: "./publish_home_view/entry.js"
|
||||
props:
|
||||
slack_bot_token:
|
||||
_enc: 3334db428df05327a883590347d39b440694f03a89b06503e73654e3596a52ab$$3g0UG9rIhQO0+wd9MbTJY9JBjSR6VpcXuem3drNL+PxsmBJzTVmt5avHP3RBbzs5m5t+mbJONef+qX1M2qvlfQZdpK9N--EzC2Qq8RMT6RKnjw--XsAu4D7SGePTz/THhitNIQ==
|
||||
|
||||
Loading…
Add table
Reference in a new issue