refactor finance.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
-- ============================================================================
|
||||
-- ROLAC — Finance mock data (奉獻 / 支出) — ONE-OFF dev seed script
|
||||
-- ============================================================================
|
||||
-- Populates ~12 months of Givings (offering sessions) and Expenses so the
|
||||
-- Finance Dashboard has trends, pie charts, and breakdowns to render.
|
||||
--
|
||||
-- WHAT IT CREATES
|
||||
-- • ~8 mock Members (mixed EN/ZH names) — so some givings link to a person.
|
||||
-- • 52 weekly OfferingSessions (one per Sunday, last 12 months), each with
|
||||
-- ~8 Giving lines across all giving categories + payment methods. About
|
||||
-- half the lines are linked to a member, the rest are anonymous.
|
||||
-- • ~12 months of Expenses across every ministry / category group, mostly
|
||||
-- Paid/Approved (so they count in the dashboard) with a few Submitted.
|
||||
--
|
||||
-- ALL rows are stamped CreatedBy = 'mockdata'. Re-running this script first
|
||||
-- DELETEs the previous mock rows, so it is SAFE TO RUN MANY TIMES.
|
||||
--
|
||||
-- PREREQUISITE: reference data must already be seeded (run the API once so
|
||||
-- DbSeeder fills GivingCategories / Ministries / ExpenseCategoryGroups /
|
||||
-- ExpenseSubCategories). This script looks those up by Name_en.
|
||||
--
|
||||
-- HOW TO RUN (dev DB = PostgreSQL "ChurchCRM" on 192.168.68.55:49154):
|
||||
-- psql "Host=192.168.68.55;Port=49154;Database=ChurchCRM;Username=chris;Password=1124" -f MockFinanceData.sql
|
||||
-- …or just paste it into pgAdmin / DBeaver against the ChurchCRM database.
|
||||
-- ============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 0. Clean up any previous mock data (order respects FKs)
|
||||
-- ---------------------------------------------------------------------------
|
||||
DELETE FROM "Givings" WHERE "CreatedBy" = 'mockdata';
|
||||
DELETE FROM "OfferingSessions" WHERE "CreatedBy" = 'mockdata';
|
||||
DELETE FROM "Expenses" WHERE "CreatedBy" = 'mockdata';
|
||||
DELETE FROM "Members" WHERE "CreatedBy" = 'mockdata';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Mock members (mixed EN/ZH) — some givings & reimbursements link to these
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO "Members"
|
||||
("FirstName_en","LastName_en","FirstName_zh","LastName_zh","Gender","Email",
|
||||
"Status","LanguagePreference","Country",
|
||||
"CreatedAt","CreatedBy","UpdatedAt","UpdatedBy","IsDeleted")
|
||||
VALUES
|
||||
('Wei', 'Chen', '偉', '陳', 'M', 'mock.wei@example.com', 'Member','zh','USA', now(),'mockdata',now(),'mockdata',false),
|
||||
('Mei', 'Lin', '美', '林', 'F', 'mock.mei@example.com', 'Member','zh','USA', now(),'mockdata',now(),'mockdata',false),
|
||||
('David', 'Wang', '大衛', '王', 'M', 'mock.david@example.com', 'Member','en','USA', now(),'mockdata',now(),'mockdata',false),
|
||||
('Grace', 'Liu', '恩典', '劉', 'F', 'mock.grace@example.com', 'Member','en','USA', now(),'mockdata',now(),'mockdata',false),
|
||||
('Samuel', 'Huang', '撒母耳','黃','M', 'mock.samuel@example.com', 'Member','zh','USA', now(),'mockdata',now(),'mockdata',false),
|
||||
('Esther', 'Wu', '以斯帖','吳','F', 'mock.esther@example.com', 'Member','zh','USA', now(),'mockdata',now(),'mockdata',false),
|
||||
('Joshua', 'Tsai', '約書亞','蔡','M', 'mock.joshua@example.com', 'Member','en','USA', now(),'mockdata',now(),'mockdata',false),
|
||||
('Hannah', 'Hsu', '漢娜', '許', 'F', 'mock.hannah@example.com', 'Member','en','USA', now(),'mockdata',now(),'mockdata',false);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Weekly offering sessions — last 52 Sundays
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- last_sunday = the most recent Sunday on/before today (dow: Sunday = 0)
|
||||
INSERT INTO "OfferingSessions"
|
||||
("SessionDate","Status","CashTotal","CheckTotal","SystemTotal","Difference",
|
||||
"CreatedAt","CreatedBy","UpdatedAt","UpdatedBy")
|
||||
SELECT w.d, 'Reconciled', 0, 0, 0, 0,
|
||||
w.d::timestamptz, 'mockdata', w.d::timestamptz, 'mockdata'
|
||||
FROM (
|
||||
SELECT ((current_date - (extract(dow from current_date))::int) - (g * 7)) AS d
|
||||
FROM generate_series(0, 51) AS g
|
||||
) w
|
||||
WHERE NOT EXISTS ( -- skip dates that already have a session
|
||||
SELECT 1 FROM "OfferingSessions" os WHERE os."SessionDate" = w.d
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Giving lines — ~8 per mock session, spread over categories & methods
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Line "specs": n, category (by Name_en), payment method, link-to-member?,
|
||||
-- amount floor, amount range.
|
||||
WITH specs(n, cat_name, method, link, amin, arange) AS (
|
||||
VALUES
|
||||
(1, 'Tithe', 'Cash', true, 100, 900),
|
||||
(2, 'Tithe', 'Check', true, 100, 900),
|
||||
(3, 'Tithe', 'Zelle', false, 80, 600),
|
||||
(4, 'General Offering', 'Cash', false, 20, 180),
|
||||
(5, 'General Offering', 'PayPal', true, 20, 180),
|
||||
(6, 'Special Offering', 'Check', true, 50, 450),
|
||||
(7, 'Building Fund', 'Zelle', true, 100, 900),
|
||||
(8, 'Mission', 'Cash', false, 30, 270)
|
||||
)
|
||||
INSERT INTO "Givings"
|
||||
("MemberId","GivingCategoryId","OfferingSessionId","Amount","PaymentMethod",
|
||||
"CheckNumber","ZelleReferenceCode","PayPalTransactionId","GivingDate",
|
||||
"IsAnonymous","Notes","CreatedAt","CreatedBy","UpdatedAt","UpdatedBy")
|
||||
SELECT
|
||||
CASE WHEN sp.link
|
||||
THEN (SELECT m."Id" FROM "Members" m WHERE m."IsDeleted" = false ORDER BY random() LIMIT 1)
|
||||
END,
|
||||
gc."Id",
|
||||
s."Id",
|
||||
round((sp.amin + random() * sp.arange))::numeric(18,2),
|
||||
sp.method,
|
||||
CASE WHEN sp.method = 'Check' THEN (1000 + (random() * 8999)::int)::text END,
|
||||
CASE WHEN sp.method = 'Zelle' THEN 'ZL' || to_char(s."SessionDate",'YYYYMMDD') || sp.n END,
|
||||
CASE WHEN sp.method = 'PayPal' THEN 'PP-' || substr(md5(random()::text), 1, 10) END,
|
||||
s."SessionDate",
|
||||
(NOT sp.link), -- anonymous when not linked to a member
|
||||
NULL,
|
||||
s."SessionDate"::timestamptz, 'mockdata', s."SessionDate"::timestamptz, 'mockdata'
|
||||
FROM "OfferingSessions" s
|
||||
CROSS JOIN specs sp
|
||||
JOIN "GivingCategories" gc ON gc."Name_en" = sp.cat_name
|
||||
WHERE s."CreatedBy" = 'mockdata';
|
||||
|
||||
-- Roll the giving lines up into each session's Cash / Check / System totals.
|
||||
UPDATE "OfferingSessions" os SET
|
||||
"CashTotal" = COALESCE(sub.cash, 0),
|
||||
"CheckTotal" = COALESCE(sub.chk, 0),
|
||||
"SystemTotal" = COALESCE(sub.sys, 0),
|
||||
"Difference" = 0
|
||||
FROM (
|
||||
SELECT "OfferingSessionId" sid,
|
||||
SUM("Amount") FILTER (WHERE "PaymentMethod" = 'Cash') AS cash,
|
||||
SUM("Amount") FILTER (WHERE "PaymentMethod" = 'Check') AS chk,
|
||||
SUM("Amount") AS sys
|
||||
FROM "Givings"
|
||||
WHERE "CreatedBy" = 'mockdata'
|
||||
GROUP BY "OfferingSessionId"
|
||||
) sub
|
||||
WHERE os."Id" = sub.sid;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Expenses — recurring monthly spend across ministries & category groups
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Spec: ministry, category group, sub-category (all by Name_en),
|
||||
-- is_reimbursement?, vendor name, description, amount floor, range.
|
||||
WITH specs(ministry, grp, sub, is_reimb, vendor, descr, amin, arange) AS (
|
||||
VALUES
|
||||
('Facility', 'Facility', 'Rent', false, 'Arcadia Property Mgmt', 'Monthly facility rent / 場地月租', 2000, 400),
|
||||
('Facility', 'Facility', 'Utilities', false, 'SoCal Edison', 'Electricity & water / 水電費', 250, 250),
|
||||
('Worship', 'Equipment', 'Maintenance & Repair', false, 'Guitar Center Service', 'Instrument/sound maintenance / 樂器維修', 80, 220),
|
||||
('Sound', 'Equipment', 'Rental', false, 'AV Rentals LA', 'AV equipment rental / 影音設備租借', 150, 350),
|
||||
('PPT/Media', 'Consumables', 'Accessories', false, 'B&H Photo', 'Cables & adapters / 線材配件', 40, 160),
|
||||
('Catering', 'Food & Beverage','Catering', false, '85C Bakery', 'Sunday fellowship meal / 主日愛筵', 200, 300),
|
||||
('Catering', 'Food & Beverage','Food Ingredients', true, NULL, 'Groceries for kitchen / 廚房食材', 80, 220),
|
||||
('Children', 'Materials', 'Craft Supplies', true, NULL, 'Sunday school crafts / 兒主手工材料', 40, 160),
|
||||
('Children', 'Materials', 'Printing', false, 'FedEx Office', 'Children lesson printing / 兒童教材印刷', 50, 150),
|
||||
('Administration','Printing', 'Bulletins', false, 'FedEx Office', 'Weekly bulletin printing / 週報印刷', 60, 120),
|
||||
('Administration','Consumables', 'Office Supplies', true, NULL, 'Office supplies / 辦公文具', 30, 120),
|
||||
('Hospitality', 'Consumables', 'Cleaning Supplies', true, NULL, 'Cleaning supplies / 清潔用品', 30, 90),
|
||||
('Preaching', 'Personnel', 'Honorarium', true, NULL, 'Guest speaker honorarium / 講員酬庸', 100, 300),
|
||||
('Administration','Missions', 'Missionary Support', false, 'OMF International', 'Monthly missionary support / 宣教士月支援', 200, 300)
|
||||
),
|
||||
-- 12 months back from the current month
|
||||
months AS (
|
||||
SELECT (date_trunc('month', current_date) - (g || ' month')::interval)::date AS m0
|
||||
FROM generate_series(0, 11) AS g
|
||||
),
|
||||
rows AS (
|
||||
SELECT
|
||||
mi."Id" AS ministry_id,
|
||||
gp."Id" AS group_id,
|
||||
sc."Id" AS sub_id,
|
||||
sp.is_reimb,
|
||||
sp.vendor,
|
||||
sp.descr,
|
||||
-- expense date: a day within that month, never in the future
|
||||
LEAST(mo.m0 + (random() * 27)::int, current_date) AS expense_date,
|
||||
round((sp.amin + random() * sp.arange))::numeric(18,2) AS amount,
|
||||
-- weighted status: 7 Paid / 2 Approved / 1 Submitted
|
||||
(ARRAY['Paid','Paid','Paid','Paid','Paid','Paid','Paid','Approved','Approved','Submitted'])
|
||||
[1 + (random() * 9)::int] AS status
|
||||
FROM specs sp
|
||||
CROSS JOIN months mo
|
||||
JOIN "Ministries" mi ON mi."Name_en" = sp.ministry
|
||||
JOIN "ExpenseCategoryGroups" gp ON gp."Name_en" = sp.grp
|
||||
JOIN "ExpenseSubCategories" sc ON sc."Name_en" = sp.sub AND sc."GroupId" = gp."Id"
|
||||
)
|
||||
INSERT INTO "Expenses"
|
||||
("MinistryId","CategoryGroupId","SubCategoryId","Type","Status","Amount",
|
||||
"Description","VendorName","MemberId","CheckNumber","ExpenseDate",
|
||||
"Notes","SubmittedBy","SubmittedAt","ReviewedBy","ReviewedAt","PaidBy","PaidAt",
|
||||
"CreatedAt","CreatedBy","UpdatedAt","UpdatedBy","IsDeleted")
|
||||
SELECT
|
||||
r.ministry_id, r.group_id, r.sub_id,
|
||||
CASE WHEN r.is_reimb THEN 'StaffReimbursement' ELSE 'VendorPayment' END,
|
||||
r.status,
|
||||
r.amount,
|
||||
r.descr,
|
||||
CASE WHEN r.is_reimb THEN NULL ELSE r.vendor END,
|
||||
CASE WHEN r.is_reimb
|
||||
THEN (SELECT m."Id" FROM "Members" m WHERE m."IsDeleted" = false ORDER BY random() LIMIT 1)
|
||||
END,
|
||||
CASE WHEN NOT r.is_reimb AND r.status = 'Paid' THEN (4000 + (random() * 5999)::int)::text END,
|
||||
r.expense_date,
|
||||
NULL,
|
||||
'mockdata', r.expense_date::timestamptz,
|
||||
CASE WHEN r.status IN ('Approved','Paid') THEN 'mockdata' END,
|
||||
CASE WHEN r.status IN ('Approved','Paid') THEN r.expense_date::timestamptz END,
|
||||
CASE WHEN r.status = 'Paid' THEN 'mockdata' END,
|
||||
CASE WHEN r.status = 'Paid' THEN r.expense_date::timestamptz END,
|
||||
r.expense_date::timestamptz, 'mockdata', r.expense_date::timestamptz, 'mockdata', false
|
||||
FROM rows r;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Quick verification (run after commit)
|
||||
-- ---------------------------------------------------------------------------
|
||||
SELECT 'members' AS kind, count(*)::text AS value FROM "Members" WHERE "CreatedBy" = 'mockdata'
|
||||
UNION ALL
|
||||
SELECT 'sessions', count(*)::text FROM "OfferingSessions" WHERE "CreatedBy" = 'mockdata'
|
||||
UNION ALL
|
||||
SELECT 'givings', count(*)::text FROM "Givings" WHERE "CreatedBy" = 'mockdata'
|
||||
UNION ALL
|
||||
SELECT 'giving $', COALESCE(sum("Amount"), 0)::numeric(18,2)::text FROM "Givings" WHERE "CreatedBy" = 'mockdata'
|
||||
UNION ALL
|
||||
SELECT 'expenses', count(*)::text FROM "Expenses" WHERE "CreatedBy" = 'mockdata'
|
||||
UNION ALL
|
||||
SELECT 'expense $ (paid+appr)', COALESCE(sum("Amount"), 0)::numeric(18,2)::text
|
||||
FROM "Expenses" WHERE "CreatedBy" = 'mockdata' AND "Status" IN ('Paid','Approved');
|
||||
Reference in New Issue
Block a user