commit 89b15af73e46c7390e9292bca2b0c3da0bb663ea Author: pdgit Date: Tue Mar 31 23:46:11 2026 +0000 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fdf6ce --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Project repo_PLTo23 diff --git a/payment-display-p_WxCpgo3/.workflow.state b/payment-display-p_WxCpgo3/.workflow.state new file mode 100644 index 0000000..17e1008 --- /dev/null +++ b/payment-display-p_WxCpgo3/.workflow.state @@ -0,0 +1,5 @@ +# DO NOT EDIT THIS FILE +--- +schema: workflow.state/2022.04 +id: p_WxCpgo3 + diff --git a/payment-display-p_WxCpgo3/calculate/entry.js b/payment-display-p_WxCpgo3/calculate/entry.js new file mode 100644 index 0000000..ef66749 --- /dev/null +++ b/payment-display-p_WxCpgo3/calculate/entry.js @@ -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, + }; + }, +}); \ No newline at end of file diff --git a/payment-display-p_WxCpgo3/filter/entry.js b/payment-display-p_WxCpgo3/filter/entry.js new file mode 100644 index 0000000..2117b20 --- /dev/null +++ b/payment-display-p_WxCpgo3/filter/entry.js @@ -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 + } +} diff --git a/payment-display-p_WxCpgo3/parse/entry.js b/payment-display-p_WxCpgo3/parse/entry.js new file mode 100644 index 0000000..563015f --- /dev/null +++ b/payment-display-p_WxCpgo3/parse/entry.js @@ -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, + }; + }, +}); \ No newline at end of file diff --git a/payment-display-p_WxCpgo3/prepare_dashboard/entry.js b/payment-display-p_WxCpgo3/prepare_dashboard/entry.js new file mode 100644 index 0000000..96e071d --- /dev/null +++ b/payment-display-p_WxCpgo3/prepare_dashboard/entry.js @@ -0,0 +1,8 @@ +export default defineComponent({ + async run({ steps }) { + return { + ...steps.calculate.$return_value, + last_updated: new Date().toISOString(), + }; + }, +}); \ No newline at end of file diff --git a/payment-display-p_WxCpgo3/workflow.yaml b/payment-display-p_WxCpgo3/workflow.yaml new file mode 100644 index 0000000..7101f17 --- /dev/null +++ b/payment-display-p_WxCpgo3/workflow.yaml @@ -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" + : '' + diff --git a/slack-app-home-p_3nC3laY/.workflow.state b/slack-app-home-p_3nC3laY/.workflow.state new file mode 100644 index 0000000..4c46b9f --- /dev/null +++ b/slack-app-home-p_3nC3laY/.workflow.state @@ -0,0 +1,5 @@ +# DO NOT EDIT THIS FILE +--- +schema: workflow.state/2022.04 +id: p_3nC3laY + diff --git a/slack-app-home-p_3nC3laY/build_home_view/entry.js b/slack-app-home-p_3nC3laY/build_home_view/entry.js new file mode 100644 index 0000000..3940d4a --- /dev/null +++ b/slack-app-home-p_3nC3laY/build_home_view/entry.js @@ -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), + ], + }, + }; + }, +}); \ No newline at end of file diff --git a/slack-app-home-p_3nC3laY/handle_slack_event/entry.js b/slack-app-home-p_3nC3laY/handle_slack_event/entry.js new file mode 100644 index 0000000..cab09ab --- /dev/null +++ b/slack-app-home-p_3nC3laY/handle_slack_event/entry.js @@ -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, + }; + }, +}); \ No newline at end of file diff --git a/slack-app-home-p_3nC3laY/publish_home_view/entry.js b/slack-app-home-p_3nC3laY/publish_home_view/entry.js new file mode 100644 index 0000000..fe98ee6 --- /dev/null +++ b/slack-app-home-p_3nC3laY/publish_home_view/entry.js @@ -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; + }, +}); \ No newline at end of file diff --git a/slack-app-home-p_3nC3laY/workflow.yaml b/slack-app-home-p_3nC3laY/workflow.yaml new file mode 100644 index 0000000..b7e0e67 --- /dev/null +++ b/slack-app-home-p_3nC3laY/workflow.yaml @@ -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== +