Files
ROLAC/API/ROLAC.API/Data/MockFinanceData.sql
T
Chris Chen 8bdb942a49
ci-cd-vm / ci-cd (push) Successful in 4m27s
update detail.
2026-06-25 09:33:49 -07:00

230 lines
13 KiB
PL/PgSQL

-- ============================================================================
-- 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,
-- pre-allocate the expense id so the matching ExpenseLine can reference it
nextval(pg_get_serial_sequence('"Expenses"','Id')) AS new_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"
)
, ins_exp AS (
INSERT INTO "Expenses"
("Id","MinistryId","Type","Status","Amount",
"Description","VendorName","MemberId","CheckNumber","ExpenseDate",
"Notes","SubmittedBy","SubmittedAt","ReviewedBy","ReviewedAt","PaidBy","PaidAt",
"CreatedAt","CreatedBy","UpdatedAt","UpdatedBy","IsDeleted")
SELECT
r.new_id, r.ministry_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
)
-- one line per mock expense (single-category), mirroring the migrated production shape
INSERT INTO "ExpenseLines"
("ExpenseId","CategoryGroupId","SubCategoryId","FunctionalClass","Amount","Description",
"CreatedAt","CreatedBy","UpdatedAt","UpdatedBy")
SELECT
r.new_id, r.group_id, r.sub_id, NULL, r.amount, NULL,
r.expense_date::timestamptz, 'mockdata', r.expense_date::timestamptz, 'mockdata'
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');