230 lines
13 KiB
PL/PgSQL
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');
|