update sql functions
This commit is contained in:
+299
-347
@@ -174,7 +174,6 @@ CREATE TABLE user_patch_tests (
|
|||||||
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
patch_test_id CHAR(12) NOT NULL REFERENCES patch_tests(id) ON DELETE CASCADE,
|
patch_test_id CHAR(12) NOT NULL REFERENCES patch_tests(id) ON DELETE CASCADE,
|
||||||
tested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
tested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
notes TEXT,
|
|
||||||
UNIQUE (user_id, patch_test_id)
|
UNIQUE (user_id, patch_test_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -453,41 +452,30 @@ GDPR COMPLIANCE NOTES:
|
|||||||
-- WHY: GDPR Article 17 - users can request data deletion
|
-- WHY: GDPR Article 17 - users can request data deletion
|
||||||
-- WHEN: User requests account deletion
|
-- WHEN: User requests account deletion
|
||||||
-- OUTPUT: Converts personal data to anonymous placeholder
|
-- OUTPUT: Converts personal data to anonymous placeholder
|
||||||
|
-- NOTE: phone/date_of_birth are NOT NULL so we use placeholder values, not NULL
|
||||||
CREATE OR REPLACE FUNCTION anonymize_user(target_id CHAR(12))
|
CREATE OR REPLACE FUNCTION anonymize_user(target_id CHAR(12))
|
||||||
RETURNS VOID AS $$
|
RETURNS VOID AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
n_first_name = 'Deleted',
|
n_first_name = 'Deleted',
|
||||||
n_last_name = 'User',
|
n_last_name = 'User',
|
||||||
email = CONCAT('deleted+', target_id, '@example.com'),
|
email = CONCAT('deleted+', target_id, '@deleted.invalid'),
|
||||||
phone = NULL,
|
phone = '+000000000000', -- NOT NULL column: use placeholder
|
||||||
profile_pic_url = NULL,
|
date_of_birth = '1900-01-01', -- NOT NULL column: use placeholder
|
||||||
date_of_birth = NULL,
|
profile_pic_url = NULL,
|
||||||
account_role = 'guest',
|
account_role = 'guest',
|
||||||
loyalty_stamps = 0,
|
loyalty_stamps = 0,
|
||||||
|
referral_code = NULL,
|
||||||
data_retention_consent = FALSE,
|
data_retention_consent = FALSE,
|
||||||
data_consent_updated_at = NOW(),
|
data_consent_updated_at = NOW(),
|
||||||
updated_at = NOW(),
|
updated_at = NOW(),
|
||||||
password_hash = NULL
|
password_hash = NULL
|
||||||
WHERE id = target_id
|
WHERE id = target_id
|
||||||
AND account_role != 'guest';
|
AND account_role != 'guest';
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- Fully delete guest user
|
|
||||||
-- WHY: Guest accounts have no ongoing business relationship
|
|
||||||
-- WHEN: Cleaning up temporary/incomplete accounts
|
|
||||||
-- OUTPUT: Complete removal from database
|
|
||||||
CREATE OR REPLACE FUNCTION delete_guest_user(target_id CHAR(12))
|
|
||||||
RETURNS VOID AS $$
|
|
||||||
BEGIN
|
|
||||||
DELETE FROM users
|
|
||||||
WHERE id = target_id
|
|
||||||
AND account_role = 'guest';
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
-- Update consent
|
-- Update consent
|
||||||
-- WHY: GDPR requires tracking consent changes
|
-- WHY: GDPR requires tracking consent changes
|
||||||
-- WHEN: User updates privacy preferences
|
-- WHEN: User updates privacy preferences
|
||||||
@@ -496,9 +484,9 @@ CREATE OR REPLACE FUNCTION update_data_consent(target_id CHAR(12), consent BOOLE
|
|||||||
RETURNS VOID AS $$
|
RETURNS VOID AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET data_retention_consent = consent,
|
SET data_retention_consent = consent,
|
||||||
data_consent_updated_at = NOW(),
|
data_consent_updated_at = NOW(),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = target_id;
|
WHERE id = target_id;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
@@ -516,75 +504,88 @@ BEGIN
|
|||||||
SELECT json_build_object(
|
SELECT json_build_object(
|
||||||
'user_profile', (
|
'user_profile', (
|
||||||
SELECT json_build_object(
|
SELECT json_build_object(
|
||||||
'id', id,
|
'id', id,
|
||||||
'first_name', n_first_name,
|
'first_name', n_first_name,
|
||||||
'last_name', n_last_name,
|
'last_name', n_last_name,
|
||||||
'full_name', fn,
|
'full_name', fn,
|
||||||
'email', email,
|
'email', email,
|
||||||
'phone', phone,
|
'phone', phone,
|
||||||
'date_of_birth', date_of_birth,
|
'date_of_birth', date_of_birth,
|
||||||
'profile_pic_url', profile_pic_url,
|
'profile_pic_url', profile_pic_url,
|
||||||
'account_role', account_role,
|
'account_role', account_role,
|
||||||
'loyalty_stamps', loyalty_stamps,
|
'loyalty_stamps', loyalty_stamps,
|
||||||
'data_retention_consent', data_retention_consent,
|
'referral_code', referral_code,
|
||||||
|
'data_retention_consent', data_retention_consent,
|
||||||
'data_consent_updated_at', data_consent_updated_at,
|
'data_consent_updated_at', data_consent_updated_at,
|
||||||
'created_at', created_at,
|
'created_at', created_at,
|
||||||
'updated_at', updated_at
|
'updated_at', updated_at
|
||||||
)
|
)
|
||||||
FROM users WHERE id = target_user_id
|
FROM users WHERE id = target_user_id
|
||||||
),
|
),
|
||||||
'bookings', (
|
'bookings', (
|
||||||
SELECT COALESCE(json_agg(
|
SELECT COALESCE(json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'booking_id', b.id,
|
'booking_id', b.id,
|
||||||
'start_time', b.start_time,
|
'start_time', b.start_time,
|
||||||
'status', b.status,
|
'status', b.status,
|
||||||
'created_at', b.created_at,
|
'notes', b.notes,
|
||||||
'updated_at', b.updated_at,
|
'created_at', b.created_at,
|
||||||
'created_by', b.created_by,
|
'updated_at', b.updated_at,
|
||||||
'updated_by', b.updated_by,
|
|
||||||
'services', (
|
'services', (
|
||||||
SELECT COALESCE(json_agg(
|
SELECT COALESCE(json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'service_id', s.id,
|
'service_id', s.id,
|
||||||
'name', s.name,
|
'name', s.name,
|
||||||
'description', s.description,
|
'description', s.description,
|
||||||
'price', s.price,
|
'price', COALESCE(bsvc.override_price, s.price),
|
||||||
'duration_minutes', s.duration_minutes
|
'duration_minutes', COALESCE(bsvc.override_duration_minutes, s.duration_minutes)
|
||||||
)
|
)
|
||||||
), '[]'::json)
|
), '[]'::json)
|
||||||
FROM booking_services bs
|
FROM booking_services bsvc
|
||||||
JOIN services s ON bs.service_id = s.id
|
JOIN services s ON bsvc.service_id = s.id
|
||||||
WHERE bs.booking_id = b.id
|
WHERE bsvc.booking_id = b.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
), '[]'::json)
|
ORDER BY b.start_time DESC), '[]'::json)
|
||||||
FROM bookings b WHERE b.user_id = target_user_id
|
FROM bookings b WHERE b.user_id = target_user_id
|
||||||
),
|
),
|
||||||
'payments', (
|
'payments', (
|
||||||
SELECT COALESCE(json_agg(
|
SELECT COALESCE(json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'payment_id', p.id,
|
'payment_id', p.id,
|
||||||
'booking_id', p.booking_id,
|
'booking_id', p.booking_id,
|
||||||
'payment_type', p.payment_type,
|
'payment_type', p.payment_type,
|
||||||
'payment_method', p.payment_method,
|
'payment_method', p.payment_method,
|
||||||
'vendor_code', p.vendor_code,
|
'invoice_number', p.invoice_number,
|
||||||
'status', p.status,
|
'status', p.status,
|
||||||
'amount', p.amount,
|
'amount', p.amount,
|
||||||
'created_at', p.created_at,
|
'vat_amount', p.vat_amount,
|
||||||
'updated_at', p.updated_at,
|
'net_amount', p.net_amount,
|
||||||
'created_by', p.created_by,
|
'created_at', p.created_at,
|
||||||
'updated_by', p.updated_by
|
'updated_at', p.updated_at
|
||||||
)
|
)
|
||||||
), '[]'::json)
|
ORDER BY p.created_at DESC), '[]'::json)
|
||||||
FROM payments p
|
FROM payments p
|
||||||
JOIN bookings b ON p.booking_id = b.id
|
JOIN bookings b ON p.booking_id = b.id
|
||||||
WHERE b.user_id = target_user_id
|
WHERE b.user_id = target_user_id
|
||||||
),
|
),
|
||||||
|
'patch_tests', (
|
||||||
|
SELECT COALESCE(json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'patch_test_id', upt.patch_test_id,
|
||||||
|
'name', pt.name,
|
||||||
|
'description', pt.description,
|
||||||
|
'tested_at', upt.tested_at
|
||||||
|
)
|
||||||
|
ORDER BY upt.tested_at DESC), '[]'::json)
|
||||||
|
FROM user_patch_tests upt
|
||||||
|
JOIN patch_tests pt ON upt.patch_test_id = pt.id
|
||||||
|
WHERE upt.user_id = target_user_id
|
||||||
|
),
|
||||||
'export_metadata', json_build_object(
|
'export_metadata', json_build_object(
|
||||||
'exported_at', NOW(),
|
'exported_at', NOW(),
|
||||||
'exported_by', 'system',
|
'exported_by', 'system',
|
||||||
'user_id', target_user_id,
|
'user_id', target_user_id,
|
||||||
'format_version', '1.0'
|
'format_version', '1.0'
|
||||||
)
|
)
|
||||||
) INTO result;
|
) INTO result;
|
||||||
@@ -612,51 +613,46 @@ TAX COMPLIANCE NOTES:
|
|||||||
-- USE: Export to QuickBooks/Xero for MTD submission to HMRC
|
-- USE: Export to QuickBooks/Xero for MTD submission to HMRC
|
||||||
CREATE OR REPLACE FUNCTION get_vat_return_data(
|
CREATE OR REPLACE FUNCTION get_vat_return_data(
|
||||||
start_date DATE,
|
start_date DATE,
|
||||||
end_date DATE
|
end_date DATE
|
||||||
)
|
)
|
||||||
RETURNS TABLE (
|
RETURNS TABLE (
|
||||||
period_start DATE,
|
period_start DATE,
|
||||||
period_end DATE,
|
period_end DATE,
|
||||||
total_sales NUMERIC(12,2),
|
total_sales NUMERIC(12,2),
|
||||||
total_vat_charged NUMERIC(12,2),
|
total_vat_charged NUMERIC(12,2),
|
||||||
net_sales NUMERIC(12,2),
|
net_sales NUMERIC(12,2),
|
||||||
transaction_count BIGINT
|
transaction_count BIGINT
|
||||||
) AS $$
|
) AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
start_date as period_start,
|
start_date AS period_start,
|
||||||
end_date as period_end,
|
end_date AS period_end,
|
||||||
COALESCE(SUM(p.amount), 0) as total_sales,
|
COALESCE(SUM(p.amount), 0) AS total_sales,
|
||||||
COALESCE(SUM(
|
COALESCE(SUM(
|
||||||
CASE
|
CASE
|
||||||
-- If VAT is explicitly stored, use it
|
|
||||||
WHEN p.vat_amount IS NOT NULL THEN p.vat_amount
|
WHEN p.vat_amount IS NOT NULL THEN p.vat_amount
|
||||||
-- If business is VAT registered but no VAT breakdown, calculate it
|
|
||||||
WHEN (SELECT is_vat_registered FROM business_settings WHERE id = 1) THEN
|
WHEN (SELECT is_vat_registered FROM business_settings WHERE id = 1) THEN
|
||||||
ROUND(p.amount - (p.amount / (1 + COALESCE(p.vat_rate, (SELECT default_vat_rate FROM business_settings WHERE id = 1)) / 100)), 2)
|
ROUND(p.amount - (p.amount / (1 + COALESCE(p.vat_rate,
|
||||||
-- Business not VAT registered = no VAT charged
|
(SELECT default_vat_rate FROM business_settings WHERE id = 1)) / 100)), 2)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END
|
END
|
||||||
), 0) as total_vat_charged,
|
), 0) AS total_vat_charged,
|
||||||
COALESCE(SUM(
|
COALESCE(SUM(
|
||||||
CASE
|
CASE
|
||||||
-- If net amount is explicitly stored, use it
|
|
||||||
WHEN p.net_amount IS NOT NULL THEN p.net_amount
|
WHEN p.net_amount IS NOT NULL THEN p.net_amount
|
||||||
-- If business is VAT registered but no net amount, calculate it
|
|
||||||
WHEN (SELECT is_vat_registered FROM business_settings WHERE id = 1) THEN
|
WHEN (SELECT is_vat_registered FROM business_settings WHERE id = 1) THEN
|
||||||
ROUND(p.amount / (1 + COALESCE(p.vat_rate,
|
ROUND(p.amount / (1 + COALESCE(p.vat_rate,
|
||||||
(SELECT default_vat_rate FROM business_settings WHERE id = 1))/100), 2)
|
(SELECT default_vat_rate FROM business_settings WHERE id = 1)) / 100), 2)
|
||||||
-- Business not VAT registered = gross amount is net amount
|
|
||||||
ELSE p.amount
|
ELSE p.amount
|
||||||
END
|
END
|
||||||
), 0) as net_sales,
|
), 0) AS net_sales,
|
||||||
COUNT(*) as transaction_count
|
COUNT(*) AS transaction_count
|
||||||
FROM payments p
|
FROM payments p
|
||||||
JOIN bookings b ON p.booking_id = b.id
|
JOIN bookings b ON p.booking_id = b.id
|
||||||
WHERE p.status = 'completed'
|
WHERE p.status = 'completed'
|
||||||
AND p.created_at::date BETWEEN start_date AND end_date
|
AND p.created_at::date BETWEEN start_date AND end_date
|
||||||
AND p.payment_type IN ('full', 'partial', 'balance');
|
AND p.payment_type IN ('full', 'partial', 'balance');
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
@@ -665,66 +661,70 @@ $$ LANGUAGE plpgsql;
|
|||||||
-- WHEN: Monthly/quarterly for accounting software import
|
-- WHEN: Monthly/quarterly for accounting software import
|
||||||
-- OUTPUT: CSV-compatible transaction list with customer, service, payment details
|
-- OUTPUT: CSV-compatible transaction list with customer, service, payment details
|
||||||
-- USE: Import directly into QuickBooks, Xero, or other accounting software
|
-- USE: Import directly into QuickBooks, Xero, or other accounting software
|
||||||
-- PARAMETERS: include_vat=false (default for micro businesses), include_vat=true only when VAT registered
|
-- NOTE: include_vat=false (default for micro businesses); set true only when VAT registered
|
||||||
CREATE OR REPLACE FUNCTION export_sales_transactions(
|
CREATE OR REPLACE FUNCTION export_sales_transactions(
|
||||||
start_date DATE,
|
start_date DATE,
|
||||||
end_date DATE,
|
end_date DATE,
|
||||||
include_vat BOOLEAN DEFAULT FALSE
|
include_vat BOOLEAN DEFAULT FALSE
|
||||||
)
|
)
|
||||||
RETURNS TABLE (
|
RETURNS TABLE (
|
||||||
transaction_date DATE,
|
transaction_date DATE,
|
||||||
invoice_number TEXT,
|
invoice_number TEXT,
|
||||||
customer_name TEXT,
|
customer_name TEXT,
|
||||||
customer_email TEXT,
|
customer_email TEXT,
|
||||||
service_description TEXT,
|
service_description TEXT,
|
||||||
payment_method TEXT,
|
payment_method TEXT,
|
||||||
gross_amount NUMERIC(10,2),
|
gross_amount NUMERIC(10,2),
|
||||||
net_amount NUMERIC(10,2),
|
net_amount NUMERIC(10,2),
|
||||||
vat_amount NUMERIC(10,2),
|
vat_amount NUMERIC(10,2),
|
||||||
vat_rate NUMERIC(5,2),
|
vat_rate NUMERIC(5,2),
|
||||||
booking_id CHAR(12),
|
booking_id CHAR(12),
|
||||||
payment_id CHAR(12)
|
payment_id CHAR(12)
|
||||||
) AS $$
|
) AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
p.created_at::date as transaction_date,
|
p.created_at::date AS transaction_date,
|
||||||
p.invoice_number::text AS invoice_number,
|
p.invoice_number::text AS invoice_number,
|
||||||
COALESCE(u.fn, 'Walk-in Customer') as customer_name,
|
COALESCE(
|
||||||
u.email as customer_email,
|
CASE WHEN u.n_first_name = 'Deleted' THEN NULL ELSE u.fn END,
|
||||||
string_agg(s.name, ', ') as service_description,
|
'Walk-in Customer'
|
||||||
p.payment_method::text as payment_method,
|
) AS customer_name,
|
||||||
p.amount as gross_amount,
|
CASE WHEN u.n_first_name = 'Deleted' THEN NULL ELSE u.email END AS customer_email,
|
||||||
|
string_agg(s.name, ', ' ORDER BY s.name) AS service_description,
|
||||||
|
p.payment_method::text AS payment_method,
|
||||||
|
p.amount AS gross_amount,
|
||||||
CASE
|
CASE
|
||||||
WHEN include_vat AND p.net_amount IS NOT NULL THEN p.net_amount
|
WHEN include_vat AND p.net_amount IS NOT NULL THEN p.net_amount
|
||||||
WHEN include_vat THEN ROUND(p.amount / 1.20, 2)
|
WHEN include_vat THEN ROUND(p.amount / 1.20, 2)
|
||||||
ELSE p.amount
|
ELSE p.amount
|
||||||
END as net_amount,
|
END AS net_amount,
|
||||||
CASE
|
CASE
|
||||||
WHEN include_vat AND p.vat_amount IS NOT NULL THEN p.vat_amount
|
WHEN include_vat AND p.vat_amount IS NOT NULL THEN p.vat_amount
|
||||||
WHEN include_vat THEN ROUND(p.amount - (p.amount / 1.20), 2)
|
WHEN include_vat THEN ROUND(p.amount - (p.amount / 1.20), 2)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as vat_amount,
|
END AS vat_amount,
|
||||||
CASE
|
CASE
|
||||||
WHEN include_vat THEN COALESCE(p.vat_rate, 20.00)
|
WHEN include_vat THEN COALESCE(p.vat_rate, 20.00)
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END as vat_rate,
|
END AS vat_rate,
|
||||||
b.id as booking_id,
|
b.id AS booking_id,
|
||||||
p.id as payment_id
|
p.id AS payment_id
|
||||||
FROM payments p
|
FROM payments p
|
||||||
JOIN bookings b ON p.booking_id = b.id
|
JOIN bookings b ON p.booking_id = b.id
|
||||||
LEFT JOIN users u ON b.user_id = u.id
|
LEFT JOIN users u ON b.user_id = u.id
|
||||||
LEFT JOIN booking_services bs ON b.id = bs.booking_id
|
LEFT JOIN booking_services bsvc ON b.id = bsvc.booking_id
|
||||||
LEFT JOIN services s ON bs.service_id = s.id
|
LEFT JOIN services s ON bsvc.service_id = s.id
|
||||||
WHERE p.status = 'completed'
|
WHERE p.status = 'completed'
|
||||||
AND p.created_at::date BETWEEN start_date AND end_date
|
AND p.created_at::date BETWEEN start_date AND end_date
|
||||||
AND p.payment_type IN ('full', 'partial', 'balance')
|
AND p.payment_type IN ('full', 'partial', 'balance')
|
||||||
GROUP BY p.id, b.id, u.fn, u.email, p.created_at, p.amount, p.net_amount, p.vat_amount, p.vat_rate, p.payment_method, p.invoice_number
|
GROUP BY p.id, b.id, u.fn, u.n_first_name, u.email,
|
||||||
|
p.created_at, p.amount, p.net_amount, p.vat_amount,
|
||||||
|
p.vat_rate, p.payment_method, p.invoice_number
|
||||||
ORDER BY p.created_at;
|
ORDER BY p.created_at;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
|
||||||
-- Monthly Business Summary
|
-- Monthly Business Summary
|
||||||
-- WHY: Track business performance trends and payment method preferences
|
-- WHY: Track business performance trends and payment method preferences
|
||||||
-- WHEN: Monthly review of business performance
|
-- WHEN: Monthly review of business performance
|
||||||
@@ -732,32 +732,32 @@ $$ LANGUAGE plpgsql;
|
|||||||
-- USE: Personal business insights - "How am I doing vs last month?"
|
-- USE: Personal business insights - "How am I doing vs last month?"
|
||||||
CREATE OR REPLACE FUNCTION get_monthly_business_summary(
|
CREATE OR REPLACE FUNCTION get_monthly_business_summary(
|
||||||
start_date DATE,
|
start_date DATE,
|
||||||
end_date DATE
|
end_date DATE
|
||||||
)
|
)
|
||||||
RETURNS TABLE (
|
RETURNS TABLE (
|
||||||
month_year TEXT,
|
month_year TEXT,
|
||||||
total_bookings BIGINT,
|
total_bookings BIGINT,
|
||||||
total_revenue NUMERIC(12,2),
|
total_revenue NUMERIC(12,2),
|
||||||
cash_payments NUMERIC(12,2),
|
cash_payments NUMERIC(12,2),
|
||||||
card_payments NUMERIC(12,2),
|
card_payments NUMERIC(12,2),
|
||||||
online_payments NUMERIC(12,2),
|
online_payments NUMERIC(12,2),
|
||||||
avg_transaction NUMERIC(10,2)
|
avg_transaction NUMERIC(10,2)
|
||||||
) AS $$
|
) AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
TO_CHAR(p.created_at, 'YYYY-MM') as month_year,
|
TO_CHAR(p.created_at, 'YYYY-MM') AS month_year,
|
||||||
COUNT(DISTINCT b.id) as total_bookings,
|
COUNT(DISTINCT b.id) AS total_bookings,
|
||||||
SUM(p.amount) as total_revenue,
|
SUM(p.amount) AS total_revenue,
|
||||||
SUM(CASE WHEN p.payment_method = 'cash' THEN p.amount ELSE 0 END) as cash_payments,
|
SUM(CASE WHEN p.payment_method = 'cash' THEN p.amount ELSE 0 END) AS cash_payments,
|
||||||
SUM(CASE WHEN p.payment_method = 'in_person_card' THEN p.amount ELSE 0 END) as card_payments,
|
SUM(CASE WHEN p.payment_method = 'in_person_card' THEN p.amount ELSE 0 END) AS card_payments,
|
||||||
SUM(CASE WHEN p.payment_method = 'online_square' THEN p.amount ELSE 0 END) as online_payments,
|
SUM(CASE WHEN p.payment_method = 'online_square' THEN p.amount ELSE 0 END) AS online_payments,
|
||||||
ROUND(AVG(p.amount), 2) as avg_transaction
|
ROUND(AVG(p.amount), 2) AS avg_transaction
|
||||||
FROM payments p
|
FROM payments p
|
||||||
JOIN bookings b ON p.booking_id = b.id
|
JOIN bookings b ON p.booking_id = b.id
|
||||||
WHERE p.status = 'completed'
|
WHERE p.status = 'completed'
|
||||||
AND p.created_at::date BETWEEN start_date AND end_date
|
AND p.created_at::date BETWEEN start_date AND end_date
|
||||||
AND p.payment_type IN ('full', 'partial', 'balance')
|
AND p.payment_type IN ('full', 'partial', 'balance')
|
||||||
GROUP BY TO_CHAR(p.created_at, 'YYYY-MM')
|
GROUP BY TO_CHAR(p.created_at, 'YYYY-MM')
|
||||||
ORDER BY month_year;
|
ORDER BY month_year;
|
||||||
END;
|
END;
|
||||||
@@ -770,28 +770,28 @@ $$ LANGUAGE plpgsql;
|
|||||||
-- USE: Homepage dashboard, "How much did I make this week?" queries
|
-- USE: Homepage dashboard, "How much did I make this week?" queries
|
||||||
CREATE OR REPLACE FUNCTION get_sales_totals(
|
CREATE OR REPLACE FUNCTION get_sales_totals(
|
||||||
start_date DATE,
|
start_date DATE,
|
||||||
end_date DATE
|
end_date DATE
|
||||||
)
|
)
|
||||||
RETURNS TABLE (
|
RETURNS TABLE (
|
||||||
total_sales NUMERIC(12,2),
|
total_sales NUMERIC(12,2),
|
||||||
total_transactions BIGINT,
|
total_transactions BIGINT,
|
||||||
cash_total NUMERIC(12,2),
|
cash_total NUMERIC(12,2),
|
||||||
card_total NUMERIC(12,2),
|
card_total NUMERIC(12,2),
|
||||||
online_total NUMERIC(12,2)
|
online_total NUMERIC(12,2)
|
||||||
) AS $$
|
) AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(p.amount), 0) as total_sales,
|
COALESCE(SUM(p.amount), 0) AS total_sales,
|
||||||
COUNT(*) as total_transactions,
|
COUNT(*) AS total_transactions,
|
||||||
COALESCE(SUM(CASE WHEN p.payment_method = 'cash' THEN p.amount ELSE 0 END), 0) as cash_total,
|
COALESCE(SUM(CASE WHEN p.payment_method = 'cash' THEN p.amount ELSE 0 END), 0) AS cash_total,
|
||||||
COALESCE(SUM(CASE WHEN p.payment_method = 'in_person_card' THEN p.amount ELSE 0 END), 0) as card_total,
|
COALESCE(SUM(CASE WHEN p.payment_method = 'in_person_card' THEN p.amount ELSE 0 END), 0) AS card_total,
|
||||||
COALESCE(SUM(CASE WHEN p.payment_method = 'online_square' THEN p.amount ELSE 0 END), 0) as online_total
|
COALESCE(SUM(CASE WHEN p.payment_method = 'online_square' THEN p.amount ELSE 0 END), 0) AS online_total
|
||||||
FROM payments p
|
FROM payments p
|
||||||
JOIN bookings b ON p.booking_id = b.id
|
JOIN bookings b ON p.booking_id = b.id
|
||||||
WHERE p.status = 'completed'
|
WHERE p.status = 'completed'
|
||||||
AND p.created_at::date BETWEEN start_date AND end_date
|
AND p.created_at::date BETWEEN start_date AND end_date
|
||||||
AND p.payment_type IN ('full', 'partial', 'balance');
|
AND p.payment_type IN ('full', 'partial', 'balance');
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
@@ -799,69 +799,57 @@ $$ LANGUAGE plpgsql;
|
|||||||
-- UTILITY FUNCTIONS
|
-- UTILITY FUNCTIONS
|
||||||
-- =======================================
|
-- =======================================
|
||||||
|
|
||||||
-- VAT Registration Transition Function
|
|
||||||
-- WHY: When crossing £90k threshold, must register for VAT and backfill existing data
|
|
||||||
-- WHEN: One-time use when VAT registration becomes mandatory
|
|
||||||
-- OUTPUT: Updates all historical payments with VAT breakdown + sets new defaults
|
|
||||||
-- USE: Call once when we register for VAT - transforms business from non-VAT to VAT
|
|
||||||
-- =======================================
|
|
||||||
-- UTILITY FUNCTIONS
|
|
||||||
-- =======================================
|
|
||||||
|
|
||||||
-- VAT Registration Transition Function
|
-- VAT Registration Transition Function
|
||||||
-- WHY: When crossing £90k threshold, must register for VAT and backfill existing data
|
-- WHY: When crossing £90k threshold, must register for VAT and backfill existing data
|
||||||
-- WHEN: One-time use when VAT registration becomes mandatory
|
-- WHEN: One-time use when VAT registration becomes mandatory
|
||||||
-- OUTPUT: Updates all historical payments with VAT breakdown + sets new defaults
|
-- OUTPUT: Updates all historical payments with VAT breakdown + sets new defaults
|
||||||
-- USE: Call once when we register for VAT - transforms business from non-VAT to VAT
|
-- USE: Call once when we register for VAT - transforms business from non-VAT to VAT
|
||||||
CREATE OR REPLACE FUNCTION enable_vat_registration(
|
CREATE OR REPLACE FUNCTION enable_vat_registration(
|
||||||
registration_date DATE DEFAULT CURRENT_DATE,
|
registration_date DATE DEFAULT CURRENT_DATE,
|
||||||
vat_rate NUMERIC(5,2) DEFAULT 20.00,
|
vat_rate NUMERIC(5,2) DEFAULT 20.00,
|
||||||
vat_reg_number TEXT DEFAULT NULL
|
vat_reg_number TEXT DEFAULT NULL
|
||||||
)
|
)
|
||||||
RETURNS TABLE (
|
RETURNS TABLE (
|
||||||
payments_updated INT,
|
payments_updated INT,
|
||||||
total_vat_calculated NUMERIC(12,2),
|
total_vat_calculated NUMERIC(12,2),
|
||||||
total_net_calculated NUMERIC(12,2),
|
total_net_calculated NUMERIC(12,2),
|
||||||
registration_effective_date DATE,
|
registration_effective_date DATE,
|
||||||
settings_updated BOOLEAN
|
settings_updated BOOLEAN
|
||||||
) AS $$
|
) AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
updated_count INT;
|
updated_count INT;
|
||||||
total_vat NUMERIC(12,2);
|
total_vat NUMERIC(12,2);
|
||||||
total_net NUMERIC(12,2);
|
total_net NUMERIC(12,2);
|
||||||
settings_ok BOOLEAN := FALSE;
|
settings_ok BOOLEAN := FALSE;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Update all existing completed payments with VAT breakdown
|
|
||||||
UPDATE payments
|
UPDATE payments
|
||||||
SET
|
SET
|
||||||
vat_rate = enable_vat_registration.vat_rate,
|
vat_rate = enable_vat_registration.vat_rate,
|
||||||
vat_amount = ROUND(amount - (amount / (1 + enable_vat_registration.vat_rate/100)), 2),
|
vat_amount = ROUND(amount - (amount / (1 + enable_vat_registration.vat_rate / 100)), 2),
|
||||||
net_amount = ROUND(amount / (1 + enable_vat_registration.vat_rate/100), 2),
|
net_amount = ROUND(amount / (1 + enable_vat_registration.vat_rate / 100), 2),
|
||||||
is_vat_applicable = TRUE,
|
is_vat_applicable = TRUE,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE status = 'completed'
|
WHERE status = 'completed'
|
||||||
AND created_at::date >= registration_date
|
AND created_at::date >= registration_date
|
||||||
AND vat_amount IS NULL;
|
AND vat_amount IS NULL;
|
||||||
|
|
||||||
GET DIAGNOSTICS updated_count = ROW_COUNT;
|
GET DIAGNOSTICS updated_count = ROW_COUNT;
|
||||||
|
|
||||||
-- Calculate totals
|
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(vat_amount), 0),
|
COALESCE(SUM(p.vat_amount), 0),
|
||||||
COALESCE(SUM(net_amount), 0)
|
COALESCE(SUM(p.net_amount), 0)
|
||||||
INTO total_vat, total_net
|
INTO total_vat, total_net
|
||||||
FROM payments
|
FROM payments p
|
||||||
WHERE status = 'completed'
|
WHERE status = 'completed'
|
||||||
AND created_at::date >= registration_date
|
AND created_at::date >= registration_date
|
||||||
AND vat_amount IS NOT NULL;
|
AND vat_amount IS NOT NULL;
|
||||||
|
|
||||||
-- Update business_settings to reflect VAT registration
|
|
||||||
UPDATE business_settings
|
UPDATE business_settings
|
||||||
SET
|
SET
|
||||||
is_vat_registered = TRUE,
|
is_vat_registered = TRUE,
|
||||||
vat_registration_number = COALESCE(vat_reg_number, vat_registration_number),
|
vat_registration_number = COALESCE(vat_reg_number, vat_registration_number),
|
||||||
default_vat_rate = enable_vat_registration.vat_rate,
|
default_vat_rate = enable_vat_registration.vat_rate,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = 1;
|
WHERE id = 1;
|
||||||
|
|
||||||
IF FOUND THEN
|
IF FOUND THEN
|
||||||
@@ -877,63 +865,51 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
|
||||||
-- Update payment with VAT (for new payments after VAT registration)
|
-- Update payment with VAT (for new payments after VAT registration)
|
||||||
-- WHY: After VAT registration, all new payments need VAT calculation
|
-- WHY: After VAT registration, all new payments need VAT calculation
|
||||||
-- WHEN: Called automatically when processing new payments (if VAT registered)
|
-- WHEN: Called when processing new payments (if VAT registered)
|
||||||
-- OUTPUT: Updates individual payment with VAT breakdown
|
-- OUTPUT: Updates individual payment with VAT breakdown
|
||||||
-- USE: Call from Go when processing new payments after VAT registration
|
-- USE: Call from Go when processing new payments after VAT registration
|
||||||
-- =======================================
|
|
||||||
-- UPDATE PAYMENT WITH VAT
|
|
||||||
-- =======================================
|
|
||||||
-- WHY: After VAT registration, all new payments need VAT calculation
|
|
||||||
-- WHEN: Called automatically when processing new payments (if VAT registered)
|
|
||||||
-- OUTPUT: Updates individual payment with VAT breakdown
|
|
||||||
-- USE: Call from Go or backend when processing new payments
|
|
||||||
CREATE OR REPLACE FUNCTION apply_vat_to_payment(
|
CREATE OR REPLACE FUNCTION apply_vat_to_payment(
|
||||||
payment_id CHAR(12),
|
payment_id CHAR(12),
|
||||||
vat_rate NUMERIC(5,2) DEFAULT NULL -- NULL = use default from business_settings
|
vat_rate NUMERIC(5,2) DEFAULT NULL -- NULL = use default from business_settings
|
||||||
)
|
)
|
||||||
RETURNS VOID AS $$
|
RETURNS VOID AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
effective_rate NUMERIC(5,2);
|
effective_rate NUMERIC(5,2);
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Determine effective VAT rate
|
|
||||||
IF vat_rate IS NULL THEN
|
IF vat_rate IS NULL THEN
|
||||||
SELECT default_vat_rate INTO effective_rate
|
SELECT default_vat_rate INTO effective_rate
|
||||||
FROM business_settings
|
FROM business_settings
|
||||||
WHERE id = 1 AND is_vat_registered = TRUE;
|
WHERE id = 1 AND is_vat_registered = TRUE;
|
||||||
|
|
||||||
IF NOT FOUND OR effective_rate IS NULL THEN
|
IF NOT FOUND OR effective_rate IS NULL THEN
|
||||||
RAISE EXCEPTION 'no vat rate provided and business is not registered for vat';
|
RAISE EXCEPTION 'No VAT rate provided and business is not registered for VAT';
|
||||||
END IF;
|
END IF;
|
||||||
ELSE
|
ELSE
|
||||||
effective_rate := vat_rate;
|
effective_rate := vat_rate;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Update payment with VAT amounts
|
|
||||||
UPDATE payments
|
UPDATE payments
|
||||||
SET
|
SET
|
||||||
vat_rate = effective_rate,
|
vat_rate = effective_rate,
|
||||||
vat_amount = ROUND(amount - (amount / (1 + effective_rate/100)), 2),
|
vat_amount = ROUND(amount - (amount / (1 + effective_rate / 100)), 2),
|
||||||
net_amount = ROUND(amount / (1 + effective_rate/100), 2),
|
net_amount = ROUND(amount / (1 + effective_rate / 100), 2),
|
||||||
is_vat_applicable = TRUE,
|
is_vat_applicable = TRUE,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = payment_id
|
WHERE id = payment_id
|
||||||
AND vat_amount IS NULL;
|
AND vat_amount IS NULL;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- =======================================
|
-- Simple VAT Calculation Helper
|
||||||
-- SIMPLE VAT CALCULATION HELPER
|
|
||||||
-- =======================================
|
|
||||||
-- WHY: Manual VAT calculations or validation
|
-- WHY: Manual VAT calculations or validation
|
||||||
-- WHEN: Checking VAT calculations or manual entry corrections
|
-- WHEN: Checking VAT calculations or manual entry corrections
|
||||||
-- OUTPUT: net amount and vat amount from gross amount
|
-- OUTPUT: net amount and vat amount from gross amount
|
||||||
-- USE: Manual calculations, validation, or corrections
|
-- USE: Manual calculations, validation, or corrections
|
||||||
CREATE OR REPLACE FUNCTION calculate_vat(
|
CREATE OR REPLACE FUNCTION calculate_vat(
|
||||||
gross_amount NUMERIC(10,2),
|
gross_amount NUMERIC(10,2),
|
||||||
vat_rate NUMERIC(5,2) DEFAULT NULL -- NULL = use default from business_settings
|
vat_rate NUMERIC(5,2) DEFAULT NULL -- NULL = use default from business_settings
|
||||||
)
|
)
|
||||||
RETURNS TABLE(
|
RETURNS TABLE(
|
||||||
net NUMERIC(10,2),
|
net NUMERIC(10,2),
|
||||||
@@ -942,49 +918,39 @@ RETURNS TABLE(
|
|||||||
DECLARE
|
DECLARE
|
||||||
effective_rate NUMERIC(5,2);
|
effective_rate NUMERIC(5,2);
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Determine effective VAT rate
|
|
||||||
IF vat_rate IS NULL THEN
|
IF vat_rate IS NULL THEN
|
||||||
SELECT default_vat_rate INTO effective_rate
|
SELECT default_vat_rate INTO effective_rate
|
||||||
FROM business_settings
|
FROM business_settings
|
||||||
WHERE id = 1 AND is_vat_registered = TRUE;
|
WHERE id = 1 AND is_vat_registered = TRUE;
|
||||||
|
|
||||||
IF NOT FOUND OR effective_rate IS NULL THEN
|
IF NOT FOUND OR effective_rate IS NULL THEN
|
||||||
RAISE EXCEPTION 'no vat rate provided and business is not registered for vat';
|
RAISE EXCEPTION 'No VAT rate provided and business is not registered for VAT';
|
||||||
END IF;
|
END IF;
|
||||||
ELSE
|
ELSE
|
||||||
effective_rate := vat_rate;
|
effective_rate := vat_rate;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Return net and VAT
|
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
ROUND(gross_amount / (1 + effective_rate/100), 2) AS net,
|
ROUND(gross_amount / (1 + effective_rate / 100), 2) AS net,
|
||||||
gross_amount - ROUND(gross_amount / (1 + effective_rate/100), 2) AS vat;
|
gross_amount - ROUND(gross_amount / (1 + effective_rate / 100), 2) AS vat;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
|
||||||
-- =======================================
|
-- =======================================
|
||||||
-- UK RECEIPT COMPLIANCE FUNCTION
|
-- UK RECEIPT COMPLIANCE FUNCTION
|
||||||
-- =======================================
|
-- =======================================
|
||||||
|
|
||||||
/*
|
/*
|
||||||
UK RECEIPT REQUIREMENTS:
|
UK RECEIPT REQUIREMENTS:
|
||||||
- Business name and address (static - provided by application)
|
- Business name and address
|
||||||
- Date and time of transaction
|
- Date and time of transaction
|
||||||
- Description of goods/services
|
- Description of goods/services
|
||||||
- Amount charged (including VAT if registered)
|
- Amount charged (including VAT if registered)
|
||||||
- VAT breakdown (if VAT registered)
|
- VAT breakdown (if VAT registered): VAT number, rate, VAT amount, net amount
|
||||||
- Receipt/invoice number
|
- Receipt/invoice number
|
||||||
- Payment method
|
- Payment method
|
||||||
- Customer details (if requested)
|
- Customer details (if not anonymised)
|
||||||
|
|
||||||
VAT RECEIPT REQUIREMENTS (if VAT registered):
|
|
||||||
- VAT registration number (static - provided by application)
|
|
||||||
- VAT rate applied
|
|
||||||
- VAT amount
|
|
||||||
- Net amount (excluding VAT)
|
|
||||||
- Total amount (including VAT)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
-- Complete Receipt Data for UK Compliance
|
-- Complete Receipt Data for UK Compliance
|
||||||
@@ -995,64 +961,62 @@ VAT RECEIPT REQUIREMENTS (if VAT registered):
|
|||||||
CREATE OR REPLACE FUNCTION get_receipt_data(p_payment_id CHAR(12))
|
CREATE OR REPLACE FUNCTION get_receipt_data(p_payment_id CHAR(12))
|
||||||
RETURNS TABLE (
|
RETURNS TABLE (
|
||||||
-- Business Information
|
-- Business Information
|
||||||
business_name TEXT,
|
business_name TEXT,
|
||||||
business_address TEXT,
|
business_address TEXT,
|
||||||
business_phone TEXT,
|
business_phone TEXT,
|
||||||
business_email TEXT,
|
business_email TEXT,
|
||||||
vat_registration_number TEXT,
|
vat_registration_number TEXT,
|
||||||
is_vat_registered BOOLEAN,
|
is_vat_registered BOOLEAN,
|
||||||
|
|
||||||
-- Payment Information
|
-- Payment Information
|
||||||
payment_id CHAR(12),
|
payment_id CHAR(12),
|
||||||
invoice_number TEXT,
|
invoice_number TEXT,
|
||||||
transaction_date TIMESTAMPTZ,
|
transaction_date TIMESTAMPTZ,
|
||||||
payment_method TEXT,
|
payment_method TEXT,
|
||||||
payment_status TEXT,
|
payment_status TEXT,
|
||||||
|
|
||||||
-- Customer Information
|
-- Customer Information
|
||||||
customer_id CHAR(12),
|
customer_id CHAR(12),
|
||||||
customer_name TEXT,
|
customer_name TEXT,
|
||||||
customer_email TEXT,
|
customer_email TEXT,
|
||||||
customer_phone TEXT,
|
customer_phone TEXT,
|
||||||
|
|
||||||
-- Booking Information
|
-- Booking Information
|
||||||
booking_id CHAR(12),
|
booking_id CHAR(12),
|
||||||
appointment_date TIMESTAMPTZ,
|
appointment_date TIMESTAMPTZ,
|
||||||
booking_status TEXT,
|
booking_status TEXT,
|
||||||
|
|
||||||
-- Service Details
|
-- Service Details
|
||||||
services JSON,
|
services JSON,
|
||||||
total_duration_minutes INT,
|
total_duration_minutes INT,
|
||||||
|
|
||||||
-- Financial Information
|
-- Financial Information
|
||||||
gross_amount NUMERIC(10,2),
|
gross_amount NUMERIC(10,2),
|
||||||
net_amount NUMERIC(10,2),
|
net_amount NUMERIC(10,2),
|
||||||
vat_amount NUMERIC(10,2),
|
vat_amount NUMERIC(10,2),
|
||||||
vat_rate NUMERIC(5,2),
|
vat_rate NUMERIC(5,2),
|
||||||
is_vat_applicable BOOLEAN,
|
is_vat_applicable BOOLEAN,
|
||||||
|
|
||||||
-- Receipt Metadata
|
-- Receipt Metadata
|
||||||
receipt_generated_at TIMESTAMPTZ,
|
receipt_generated_at TIMESTAMPTZ,
|
||||||
currency_code TEXT
|
currency_code TEXT
|
||||||
) AS $$
|
) AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
bs RECORD; -- To hold business settings
|
bs RECORD;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Fetch current business settings (assume single row, id=1)
|
|
||||||
SELECT INTO bs
|
SELECT INTO bs
|
||||||
business_name,
|
bs2.business_name,
|
||||||
business_address,
|
bs2.business_address,
|
||||||
business_phone,
|
bs2.business_phone,
|
||||||
business_email,
|
bs2.business_email,
|
||||||
vat_registration_number,
|
bs2.vat_registration_number,
|
||||||
is_vat_registered,
|
bs2.is_vat_registered,
|
||||||
currency_code
|
bs2.currency_code
|
||||||
FROM business_settings
|
FROM business_settings bs2
|
||||||
WHERE id = 1;
|
WHERE bs2.id = 1;
|
||||||
|
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
-- Business Info
|
|
||||||
bs.business_name,
|
bs.business_name,
|
||||||
bs.business_address,
|
bs.business_address,
|
||||||
bs.business_phone,
|
bs.business_phone,
|
||||||
@@ -1060,68 +1024,67 @@ BEGIN
|
|||||||
bs.vat_registration_number,
|
bs.vat_registration_number,
|
||||||
bs.is_vat_registered,
|
bs.is_vat_registered,
|
||||||
|
|
||||||
-- Payment Info
|
p.id AS payment_id,
|
||||||
p.id as payment_id,
|
COALESCE('INV-' || p.invoice_number::text, 'INV-' || p.id) AS invoice_number,
|
||||||
COALESCE('INV-' || p.invoice_number::text, 'INV-' || p.id) as invoice_number,
|
p.created_at AS transaction_date,
|
||||||
p.created_at as transaction_date,
|
p.payment_method::text AS payment_method,
|
||||||
p.payment_method::text as payment_method,
|
p.status::text AS payment_status,
|
||||||
p.status::text as payment_status,
|
|
||||||
|
|
||||||
-- Customer Info
|
u.id AS customer_id,
|
||||||
u.id as customer_id,
|
|
||||||
CASE
|
CASE
|
||||||
WHEN u.account_role = 'guest' OR u.fn = 'Deleted User' THEN 'Walk-in Customer'
|
WHEN u.id IS NULL THEN 'Walk-in Customer'
|
||||||
|
WHEN u.n_first_name = 'Deleted' THEN 'Walk-in Customer'
|
||||||
|
WHEN u.account_role = 'guest' THEN 'Walk-in Customer'
|
||||||
ELSE u.fn
|
ELSE u.fn
|
||||||
END as customer_name,
|
END AS customer_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN u.account_role = 'guest' OR u.fn = 'Deleted User' THEN NULL
|
WHEN u.id IS NULL THEN NULL
|
||||||
|
WHEN u.n_first_name = 'Deleted' THEN NULL
|
||||||
|
WHEN u.account_role = 'guest' THEN NULL
|
||||||
ELSE u.email
|
ELSE u.email
|
||||||
END as customer_email,
|
END AS customer_email,
|
||||||
CASE
|
CASE
|
||||||
WHEN u.account_role = 'guest' OR u.fn = 'Deleted User' THEN NULL
|
WHEN u.id IS NULL THEN NULL
|
||||||
|
WHEN u.n_first_name = 'Deleted' THEN NULL
|
||||||
|
WHEN u.account_role = 'guest' THEN NULL
|
||||||
ELSE u.phone
|
ELSE u.phone
|
||||||
END as customer_phone,
|
END AS customer_phone,
|
||||||
|
|
||||||
-- Booking Info
|
b.id AS booking_id,
|
||||||
b.id as booking_id,
|
b.start_time AS appointment_date,
|
||||||
b.start_time as appointment_date,
|
b.status::text AS booking_status,
|
||||||
b.status::text as booking_status,
|
|
||||||
|
|
||||||
-- Service Details
|
|
||||||
COALESCE((
|
COALESCE((
|
||||||
SELECT json_agg(
|
SELECT json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'service_id', s.id,
|
'service_id', s.id,
|
||||||
'name', s.name,
|
'name', s.name,
|
||||||
'description', s.description,
|
'description', s.description,
|
||||||
'price', s.price,
|
'price', COALESCE(bsvc.override_price, s.price),
|
||||||
'duration_minutes', s.duration_minutes
|
'duration_minutes', COALESCE(bsvc.override_duration_minutes, s.duration_minutes)
|
||||||
)
|
)
|
||||||
ORDER BY s.name
|
ORDER BY s.name
|
||||||
)
|
)
|
||||||
FROM booking_services bs_inner
|
FROM booking_services bsvc
|
||||||
JOIN services s ON bs_inner.service_id = s.id
|
JOIN services s ON bsvc.service_id = s.id
|
||||||
WHERE bs_inner.booking_id = b.id
|
WHERE bsvc.booking_id = b.id
|
||||||
), '[]'::json) as services,
|
), '[]'::json) AS services,
|
||||||
|
|
||||||
-- Total service duration
|
|
||||||
COALESCE((
|
COALESCE((
|
||||||
SELECT SUM(s.duration_minutes)
|
SELECT SUM(COALESCE(bsvc.override_duration_minutes, s.duration_minutes))
|
||||||
FROM booking_services bs_inner
|
FROM booking_services bsvc
|
||||||
JOIN services s ON bs_inner.service_id = s.id
|
JOIN services s ON bsvc.service_id = s.id
|
||||||
WHERE bs_inner.booking_id = b.id
|
WHERE bsvc.booking_id = b.id
|
||||||
), 0) as total_duration_minutes,
|
), 0)::INT AS total_duration_minutes,
|
||||||
|
|
||||||
-- Financial Info
|
p.amount AS gross_amount,
|
||||||
p.amount as gross_amount,
|
COALESCE(p.net_amount, p.amount) AS net_amount,
|
||||||
COALESCE(p.net_amount, p.amount) as net_amount,
|
COALESCE(p.vat_amount, 0.00) AS vat_amount,
|
||||||
COALESCE(p.vat_amount, 0.00) as vat_amount,
|
p.vat_rate AS vat_rate,
|
||||||
p.vat_rate as vat_rate,
|
p.is_vat_applicable AS is_vat_applicable,
|
||||||
p.is_vat_applicable as is_vat_applicable,
|
|
||||||
|
|
||||||
-- Receipt Metadata
|
NOW() AS receipt_generated_at,
|
||||||
NOW() as receipt_generated_at,
|
bs.currency_code AS currency_code
|
||||||
bs.currency_code as currency_code
|
|
||||||
|
|
||||||
FROM payments p
|
FROM payments p
|
||||||
JOIN bookings b ON p.booking_id = b.id
|
JOIN bookings b ON p.booking_id = b.id
|
||||||
@@ -1134,73 +1097,62 @@ $$ LANGUAGE plpgsql;
|
|||||||
-- FUNCTION USAGE SUMMARY
|
-- FUNCTION USAGE SUMMARY
|
||||||
-- =======================================
|
-- =======================================
|
||||||
/*
|
/*
|
||||||
This summary organizes all database functions by their primary purpose, legal basis, and expected usage frequency—
|
This summary organises all database functions by purpose, legal basis, and expected
|
||||||
providing a clear guide for integration into our application, compliance workflows, and business operations.
|
usage frequency — providing a clear guide for application integration.
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
1. FINANCIAL & TAX COMPLIANCE
|
1. FINANCIAL & TAX COMPLIANCE
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
REGULAR USE (called from API endpoints or UI):
|
REGULAR USE (called from API endpoints):
|
||||||
- get_sales_totals(start_date, end_date)
|
- get_sales_totals(start_date, end_date)
|
||||||
→ Quick dashboard metrics: total revenue, transaction count, and payment method breakdown.
|
→ Dashboard metrics: total revenue, transaction count, payment method breakdown.
|
||||||
- get_receipt_data(payment_id)
|
- get_receipt_data(payment_id)
|
||||||
→ Generate a UK-compliant receipt after every completed payment.
|
→ UK-compliant receipt after every completed payment.
|
||||||
- calculate_vat(gross_amount, vat_rate)
|
- calculate_vat(gross_amount, vat_rate)
|
||||||
→ Utility for validating or manually computing VAT splits.
|
→ Validate or manually compute VAT splits (only usable when VAT registered).
|
||||||
|
|
||||||
PERIODIC USE (monthly/quarterly for accounting or tax filing):
|
PERIODIC USE (monthly/quarterly for accounting or HMRC):
|
||||||
- export_sales_transactions(start_date, end_date, include_vat)
|
- export_sales_transactions(start_date, end_date, include_vat)
|
||||||
→ Primary export for accounting software (QuickBooks, Xero). Set include_vat = true only if VAT-registered.
|
→ Primary export for QuickBooks / Xero. Set include_vat=true only if VAT registered.
|
||||||
- get_monthly_business_summary(start_date, end_date)
|
- get_monthly_business_summary(start_date, end_date)
|
||||||
→ Analyze trends: bookings, revenue, avg. transaction size, and payment method adoption.
|
→ Revenue trends, avg. transaction, and payment method adoption month by month.
|
||||||
- get_vat_return_data(start_date, end_date)
|
- get_vat_return_data(start_date, end_date)
|
||||||
→ MTD-ready VAT return summary for HMRC (only if VAT-registered).
|
→ MTD-ready VAT return summary for HMRC (only relevant when VAT registered).
|
||||||
|
|
||||||
ONE-TIME OR TRANSITIONAL USE:
|
ONE-TIME / TRANSITIONAL:
|
||||||
- enable_vat_registration(registration_date, vat_rate, vat_reg_number)
|
- enable_vat_registration(registration_date, vat_rate, vat_reg_number)
|
||||||
→ Run once when crossing the £90k VAT threshold to backfill historical payments.
|
→ Run once on crossing the £90k VAT threshold; backfills historical payments.
|
||||||
- apply_vat_to_payment(payment_id, vat_rate)
|
- apply_vat_to_payment(payment_id, vat_rate)
|
||||||
→ Called automatically for every new payment after VAT registration.
|
→ Called for every new payment after VAT registration.
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
2. GDPR & DATA PRIVACY
|
2. GDPR & DATA PRIVACY
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
ON-DEMAND USE (triggered by user request or admin action):
|
ON-DEMAND (triggered by user request or admin action):
|
||||||
- export_all_user_data(user_id)
|
- export_all_user_data(user_id)
|
||||||
→ Full Subject Access Request (SAR) export in JSON (GDPR Article 15).
|
→ Full Subject Access Request (SAR) export in JSON — GDPR Article 15.
|
||||||
|
→ Includes profile, bookings (with service overrides), payments, patch tests.
|
||||||
- anonymize_user(user_id)
|
- anonymize_user(user_id)
|
||||||
→ Right to Erasure for registered users (GDPR Article 17); preserves audit trail.
|
→ Right to Erasure for registered users — GDPR Article 17.
|
||||||
|
→ Preserves booking/payment audit trail; replaces PII with placeholders.
|
||||||
|
→ NOTE: phone and date_of_birth use placeholder values (NOT NULL columns).
|
||||||
- delete_guest_user(user_id)
|
- delete_guest_user(user_id)
|
||||||
→ Complete deletion of guest accounts (no contractual basis).
|
→ Complete deletion of guest accounts (no contractual retention basis).
|
||||||
- update_data_consent(user_id, consent)
|
- update_data_consent(user_id, consent)
|
||||||
→ Records updated consent preference with timestamp for auditability.
|
→ Records updated consent with timestamp for auditability.
|
||||||
|
|
||||||
NOTE: Booking data is retained under contractual necessity (GDPR Art. 6(1)(b)) even if consent is withdrawn.
|
NOTE: Booking/payment data is retained under contractual necessity (GDPR Art. 6(1)(b))
|
||||||
Only optional data (e.g., marketing) is governed by consent.
|
even after consent withdrawal. Only optional/marketing data is consent-governed.
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
3. BUSINESS OPERATIONS & LOYALTY
|
3. INTEGRATION QUICK REFERENCE
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
• User deletes account → anonymize_user() or delete_guest_user()
|
||||||
- Referral Program: Use user_referrals table + referral_code in users (handled in app logic).
|
|
||||||
- Patch Test Tracking: user_service_patch_tests enforces allergen safety (enforced in app based on services.patch_test_duration_hours).
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
4. UTILITY & MAINTENANCE
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
- generate_*_id() functions: Internal use only (DEFAULT in table definitions).
|
|
||||||
- Timestamp triggers (e.g., on business_settings): Automatic—no manual call needed.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
INTEGRATION QUICK REFERENCE
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
• User deletes account → anonymize_user() or delete_guest_user()
|
|
||||||
• "What data do you have?" → export_all_user_data()
|
• "What data do you have?" → export_all_user_data()
|
||||||
• New payment → apply_vat_to_payment() (if VAT reg) + get_receipt_data()
|
• New payment → apply_vat_to_payment() (if VAT reg) + get_receipt_data()
|
||||||
• Monthly close → export_sales_transactions(..., include_vat := [true/false])
|
• Monthly close → export_sales_transactions(..., include_vat := [true/false])
|
||||||
• VAT registration day → enable_vat_registration() once, then apply_vat_to_payment()
|
• VAT registration day → enable_vat_registration() once, then apply_vat_to_payment()
|
||||||
• Dashboard load → get_sales_totals() + get_monthly_business_summary()
|
• Dashboard load → get_sales_totals() + get_monthly_business_summary()
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user