Changes between Initial Version and Version 1 of AdvancedReports


Ignore:
Timestamp:
05/24/26 16:38:26 (36 hours ago)
Author:
202033
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedReports

    v1 v1  
     1= Напредни извештаи од базата (SQL, складирани процедури и релациона алгебра)
     2
     3== 1. Месечни приходи и перформанси на плаќања
     4  По месец прикажува колку плаќања има, колку се успешни/неуспешни, колку е приходот од термини наспроти приходи од купување пакет, и месечна
     5  промена на приход.
     6=== SQL
     7{{{
     8WITH bounds AS (
     9  SELECT
     10    date_trunc('month', MIN(p."timestamp"))::date AS min_month,
     11    date_trunc('month', MAX(p."timestamp"))::date AS max_month
     12  FROM payment p
     13),
     14months AS (
     15  SELECT generate_series(min_month, max_month, interval '1 month')::date AS month_start
     16  FROM bounds
     17),
     18pay AS (
     19  SELECT
     20    date_trunc('month', p."timestamp")::date AS month_start,
     21    p.status,
     22    p.amount,
     23    p.appointment_id,
     24    p.package_purchase_id
     25  FROM payment p
     26),
     27agg AS (
     28  SELECT
     29    m.month_start,
     30    COUNT(pay.*) AS payment_cnt,
     31    COUNT(*) FILTER (WHERE pay.status = 'PAID') AS paid_cnt,
     32    COUNT(*) FILTER (WHERE pay.status = 'PENDING') AS pending_cnt,
     33    COUNT(*) FILTER (WHERE pay.status = 'FAILED') AS failed_cnt,
     34    COUNT(*) FILTER (WHERE pay.status = 'REFUNDED') AS refunded_cnt,
     35    COALESCE(SUM(pay.amount) FILTER (WHERE pay.status = 'PAID'), 0)::numeric(12,2) AS paid_amount_total,
     36    COALESCE(SUM(pay.amount) FILTER (WHERE pay.status = 'REFUNDED'), 0)::numeric(12,2) AS refunded_amount_total,
     37    COALESCE(SUM(pay.amount) FILTER (WHERE pay.status = 'PAID' AND pay.appointment_id IS NOT NULL), 0)::numeric(12,2) AS paid_appointment_amount,
     38    COALESCE(SUM(pay.amount) FILTER (WHERE pay.status = 'PAID' AND pay.appointment_id IS NULL AND pay.package_purchase_id IS NOT NULL), 0)::numeric(12,2) AS paid_package_amount
     39  FROM months m
     40  LEFT JOIN pay ON pay.month_start = m.month_start
     41  GROUP BY m.month_start
     42)
     43SELECT
     44  a.month_start,
     45  a.payment_cnt,
     46  a.paid_cnt,
     47  a.pending_cnt,
     48  a.failed_cnt,
     49  a.refunded_cnt,
     50  a.paid_amount_total,
     51  a.refunded_amount_total,
     52  a.paid_appointment_amount,
     53  a.paid_package_amount,
     54  ROUND(100.0 * a.paid_cnt / NULLIF(a.payment_cnt, 0), 2) AS paid_rate_pct,
     55  ROUND(100.0 * a.failed_cnt / NULLIF(a.payment_cnt, 0), 2) AS failed_rate_pct,
     56  ROUND(
     57    a.paid_amount_total
     58    - COALESCE(LAG(a.paid_amount_total) OVER (ORDER BY a.month_start), 0),
     59    2
     60  ) AS mom_paid_amount_change
     61FROM agg a
     62ORDER BY a.month_start;
     63
     64}}}
     65
     66=== Релациона алгебра
     67
     68
     69{{{
     70B ← γ_{min_m:=min(month(ts)), max_m:=max(month(ts))}(Payment)
     71M ← generate_series(B.min_m, B.max_m, 1 month)
     72P ← π_{month:=month(ts), status, amount, appointment_id, package_purchase_id}(Payment)
     73R ← γ_{month; cnt:=count(*), paid:=count_{status='PAID'}(*), pending:=count_{status='PENDING'}(*), failed:=count_{status='FAILED'}(*), refunded:=count_{status='REFUNDED'}(*), paid_sum:=sum_{status='PAID'}(amount), paid_appt:=sum_{status='PAID'∧appointment_id≠NULL}(amount), paid_pkg:=sum_{status='PAID'∧appointment_id=NULL∧package_purchase_id≠NULL}(amount)}(M ⟕_{M.month=P.month} P)
     74}}}
     75
     76Овој извештај веднаш покажува дали растот доаѓа од класични термини или од продажба на пакети и дали има пад на success rate по месеци.
     77
     78
     79
     80== 2. Најпопуларни услуги според закажувања, приход и оценки
     81
     82 За секоја услуга покажува број на закажувања по статус, алоциран платен приход, просечна оценка и број рецензии, со ранг на популарност.
     83
     84=== SQL
     85
     86{{{
     87WITH appointment_base AS (
     88  SELECT
     89    a.appointment_id,
     90    UPPER(s.name) AS status_name
     91  FROM appointment a
     92  JOIN status s ON s.status_id = a.status_id
     93),
     94service_lines AS (
     95  SELECT
     96    aps.appointment_id,
     97    aps.service_id,
     98    sv.name AS service_name,
     99    c.name AS category_name,
     100    sv.price AS service_list_price
     101  FROM appointmentservice aps
     102  JOIN service sv ON sv.service_id = aps.service_id
     103  LEFT JOIN category c ON c.category_id = sv.category_id
     104),
     105appointment_list_totals AS (
     106  SELECT
     107    sl.appointment_id,
     108    SUM(sl.service_list_price) AS list_total_price
     109  FROM service_lines sl
     110  GROUP BY sl.appointment_id
     111),
     112paid_amount_per_appt AS (
     113  SELECT
     114    p.appointment_id,
     115    MAX(p.amount) AS paid_amount
     116  FROM payment p
     117  WHERE p.status = 'PAID'
     118    AND p.appointment_id IS NOT NULL
     119  GROUP BY p.appointment_id
     120),
     121revenue_alloc AS (
     122  SELECT
     123    sl.appointment_id,
     124    sl.service_id,
     125    (pa.paid_amount * sl.service_list_price / NULLIF(alt.list_total_price, 0))::numeric(12,2) AS allocated_amount
     126  FROM service_lines sl
     127  JOIN appointment_list_totals alt ON alt.appointment_id = sl.appointment_id
     128  JOIN paid_amount_per_appt pa ON pa.appointment_id = sl.appointment_id
     129),
     130reviews_by_service AS (
     131  SELECT
     132    aps.service_id,
     133    ROUND(AVG(r.rating)::numeric, 2) AS avg_rating,
     134    COUNT(r.review_id) AS review_count
     135  FROM appointmentservice aps
     136  JOIN payment p
     137    ON p.appointment_id = aps.appointment_id
     138   AND p.status = 'PAID'
     139  JOIN review r ON r.payment_id = p.payment_id
     140  GROUP BY aps.service_id
     141),
     142service_stats AS (
     143  SELECT
     144    sl.service_id,
     145    sl.service_name,
     146    sl.category_name,
     147    COUNT(*) AS bookings_total,
     148    COUNT(*) FILTER (WHERE ab.status_name = 'COMPLETED') AS completed_bookings,
     149    COUNT(*) FILTER (WHERE ab.status_name = 'SCHEDULED') AS scheduled_bookings,
     150    COUNT(*) FILTER (WHERE ab.status_name = 'CANCELLED') AS cancelled_bookings,
     151    COUNT(*) FILTER (WHERE ab.status_name = 'NO_SHOW') AS no_show_bookings,
     152    COALESCE(SUM(ra.allocated_amount), 0)::numeric(12,2) AS allocated_paid_revenue
     153  FROM service_lines sl
     154  JOIN appointment_base ab ON ab.appointment_id = sl.appointment_id
     155  LEFT JOIN revenue_alloc ra
     156    ON ra.appointment_id = sl.appointment_id
     157   AND ra.service_id = sl.service_id
     158  GROUP BY sl.service_id, sl.service_name, sl.category_name
     159)
     160SELECT
     161  ss.service_id,
     162  ss.service_name,
     163  ss.category_name,
     164  ss.bookings_total,
     165  ss.completed_bookings,
     166  ss.scheduled_bookings,
     167  ss.cancelled_bookings,
     168  ss.no_show_bookings,
     169  ss.allocated_paid_revenue,
     170  COALESCE(rv.avg_rating, 0)::numeric(4,2) AS avg_rating,
     171  COALESCE(rv.review_count, 0) AS review_count,
     172  DENSE_RANK() OVER (
     173    ORDER BY ss.completed_bookings DESC, ss.allocated_paid_revenue DESC
     174  ) AS popularity_rank
     175FROM service_stats ss
     176LEFT JOIN reviews_by_service rv ON rv.service_id = ss.service_id
     177ORDER BY popularity_rank, ss.service_id;
     178
     179}}}
     180
     181
     182=== Релациона алгебра
     183
     184{{{
     185SL ← AppointmentService ⨝ Service ⨝ Category
     186AB ← Appointment ⨝ Status
     187T ← γ_{appointment_id; list_total:=Σ(service_price)}(SL)
     188PP ← σ_{status='PAID' ∧ appointment_id≠NULL}(Payment)
     189RA ← γ_{appointment_id, service_id; alloc:=paid_amount*service_price/list_total}(SL ⨝ T ⨝ PP)
     190RV ← γ_{service_id; avg_rating:=AVG(rating), review_count:=COUNT(*)}(AppointmentService ⨝ PP ⨝ Review)
     191S ← γ_{service_id, service_name, category_name; bookings:=COUNT(*), completed:=COUNT_{status='COMPLETED'}(*), revenue:=Σ(alloc)}(SL ⨝ AB ⟕ RA)
     192Result ← S ⟕ RV
     193}}}
     194
     195Ова е најкорисен top services извештај за одлуки, кои услуги се чести, кои носат реален приход и како се оценети од клиенти.
     196
     197
     198== 3. Лојалност и активност на клиенти
     199
     200По корисник дава извештај за активност со термини, платени/неуспешни плаќања, купени пакети, искористеност и моментални loyalty поени.
     201
     202=== SQL
     203
     204{{{
     205WITH appt_agg AS (
     206  SELECT
     207    a.user_id,
     208    COUNT(*) AS appointments_total,
     209    COUNT(*) FILTER (WHERE UPPER(s.name) = 'SCHEDULED') AS scheduled_cnt,
     210    COUNT(*) FILTER (WHERE UPPER(s.name) = 'COMPLETED') AS completed_cnt,
     211    COUNT(*) FILTER (WHERE UPPER(s.name) = 'CANCELLED') AS cancelled_cnt,
     212    COUNT(*) FILTER (WHERE UPPER(s.name) = 'NO_SHOW') AS no_show_cnt,
     213    MAX(a.appointment_time) AS last_appointment_at
     214  FROM appointment a
     215  JOIN status s ON s.status_id = a.status_id
     216  GROUP BY a.user_id
     217),
     218pay_user_map AS (
     219  SELECT
     220    p.payment_id,
     221    COALESCE(a.user_id, upp.user_id) AS user_id,
     222    p.status,
     223    p.amount,
     224    p.points_used,
     225    p.appointment_id,
     226    p.package_purchase_id,
     227    p."timestamp"
     228  FROM payment p
     229  LEFT JOIN appointment a ON a.appointment_id = p.appointment_id
     230  LEFT JOIN userpackagepurchase upp ON upp.purchase_id = p.package_purchase_id
     231),
     232pay_agg AS (
     233  SELECT
     234    pum.user_id,
     235    COUNT(*) AS payments_total,
     236    COUNT(*) FILTER (WHERE pum.status = 'PAID') AS paid_payments_cnt,
     237    COUNT(*) FILTER (WHERE pum.status = 'FAILED') AS failed_payments_cnt,
     238    COUNT(*) FILTER (WHERE pum.status = 'REFUNDED') AS refunded_payments_cnt,
     239    COALESCE(SUM(pum.amount) FILTER (WHERE pum.status = 'PAID'), 0)::numeric(12,2) AS paid_amount_total,
     240    COALESCE(SUM(pum.amount) FILTER (WHERE pum.status = 'PAID' AND pum.appointment_id IS NOT NULL), 0)::numeric(12,2) AS paid_appointment_amount,
     241    COALESCE(SUM(pum.amount) FILTER (WHERE pum.status = 'PAID' AND pum.appointment_id IS NULL AND pum.package_purchase_id IS NOT NULL), 0)::numeric(12,2) AS paid_package_amount,
     242    COALESCE(SUM(pum.points_used) FILTER (WHERE pum.status = 'PAID'), 0) AS points_used_total,
     243    MAX(pum."timestamp") AS last_payment_at
     244  FROM pay_user_map pum
     245  WHERE pum.user_id IS NOT NULL
     246  GROUP BY pum.user_id
     247),
     248pkg_agg AS (
     249  SELECT
     250    upp.user_id,
     251    COUNT(*) AS package_purchases_total,
     252    COUNT(*) FILTER (WHERE upp.status = 'ACTIVE') AS active_packages_cnt,
     253    COALESCE(SUM(upp.total_uses), 0) AS total_package_uses_bought,
     254    COALESCE(SUM(upp.remaining_uses), 0) AS total_package_uses_remaining,
     255    COALESCE(SUM(upp.total_uses - upp.remaining_uses), 0) AS total_package_uses_used,
     256    MAX(upp.purchased_at) AS last_package_purchase_at
     257  FROM userpackagepurchase upp
     258  GROUP BY upp.user_id
     259)
     260SELECT
     261  u.user_id,
     262  u.full_name,
     263  u.email,
     264  COALESCE(lc.points, 0) AS loyalty_points_balance,
     265  COALESCE(a.appointments_total, 0) AS appointments_total,
     266  COALESCE(a.completed_cnt, 0) AS completed_cnt,
     267  COALESCE(a.scheduled_cnt, 0) AS scheduled_cnt,
     268  COALESCE(a.cancelled_cnt, 0) AS cancelled_cnt,
     269  COALESCE(a.no_show_cnt, 0) AS no_show_cnt,
     270  ROUND(100.0 * COALESCE(a.completed_cnt, 0) / NULLIF(COALESCE(a.appointments_total, 0), 0), 2) AS completion_rate_pct,
     271  COALESCE(p.payments_total, 0) AS payments_total,
     272  COALESCE(p.paid_payments_cnt, 0) AS paid_payments_cnt,
     273  COALESCE(p.failed_payments_cnt, 0) AS failed_payments_cnt,
     274  COALESCE(p.refunded_payments_cnt, 0) AS refunded_payments_cnt,
     275  COALESCE(p.paid_amount_total, 0)::numeric(12,2) AS paid_amount_total,
     276  COALESCE(p.paid_appointment_amount, 0)::numeric(12,2) AS paid_appointment_amount,
     277  COALESCE(p.paid_package_amount, 0)::numeric(12,2) AS paid_package_amount,
     278  COALESCE(p.points_used_total, 0) AS points_used_total,
     279  COALESCE(pk.package_purchases_total, 0) AS package_purchases_total,
     280  COALESCE(pk.active_packages_cnt, 0) AS active_packages_cnt,
     281  COALESCE(pk.total_package_uses_bought, 0) AS total_package_uses_bought,
     282  COALESCE(pk.total_package_uses_used, 0) AS total_package_uses_used,
     283  COALESCE(pk.total_package_uses_remaining, 0) AS total_package_uses_remaining,
     284  ROW_NUMBER() OVER (
     285    ORDER BY COALESCE(p.paid_amount_total, 0) DESC, COALESCE(lc.points, 0) DESC, u.user_id
     286  ) AS value_rank
     287FROM "User" u
     288LEFT JOIN loyaltycard lc ON lc.user_id = u.user_id
     289LEFT JOIN appt_agg a ON a.user_id = u.user_id
     290LEFT JOIN pay_agg p ON p.user_id = u.user_id
     291LEFT JOIN pkg_agg pk ON pk.user_id = u.user_id
     292ORDER BY value_rank;
     293
     294}}}
     295
     296=== Релациона алгебра
     297
     298{{{
     299A ← γ_{user_id; appt_total:=COUNT(*), completed:=COUNT_{status='COMPLETED'}(*), ...}(Appointment ⨝ Status)
     300P0 ← Payment ⟕ Appointment ⟕ UserPackagePurchase
     301P ← γ_{user_id; paid_sum:=Σ_{status='PAID'}(amount), paid_cnt:=COUNT_{status='PAID'}(*), ...}(P0)
     302K ← γ_{user_id; pkg_cnt:=COUNT(*), active_pkg:=COUNT_{status='ACTIVE'}(*), uses_bought:=Σ(total_uses), uses_rem:=Σ(remaining_uses)}(UserPackagePurchase)
     303Result ← User ⟕ LoyaltyCard ⟕ A ⟕ P ⟕ K
     304}}}
     305
     306Извештајот е добар за CRM сегментација: брзо се гледа кои клиенти носат најмногу приход, колку се активни и каков им е loyalty балансот.
     307
     308
     309== 4. Продажба и искористеност на пакети
     310За секој пакет покажува колку е продаден, колку е исплатен, колку користења се потрошени/останати и колку е „популарен“ во реална употреба.
     311
     312=== SQL
     313
     314{{{
     315WITH package_base AS (
     316  SELECT
     317    p.package_id,
     318    p.name AS package_name,
     319    p.max_usage,
     320    p.total_price,
     321    COUNT(ps.service_id) AS services_in_package,
     322    COALESCE(SUM(ps.discounted_price), 0)::numeric(12,2) AS computed_services_price_sum
     323  FROM package p
     324  LEFT JOIN packageservice ps ON ps.package_id = p.package_id
     325  GROUP BY p.package_id, p.name, p.max_usage, p.total_price
     326),
     327purchase_stats AS (
     328  SELECT
     329    upp.package_id,
     330    COUNT(*) AS purchases_cnt,
     331    COUNT(*) FILTER (WHERE upp.status = 'ACTIVE') AS active_purchases_cnt,
     332    COUNT(*) FILTER (WHERE upp.status = 'EXPIRED') AS expired_purchases_cnt,
     333    COUNT(*) FILTER (WHERE upp.status = 'CANCELLED') AS cancelled_purchases_cnt,
     334    COALESCE(SUM(upp.total_uses), 0) AS total_uses_sold,
     335    COALESCE(SUM(upp.remaining_uses), 0) AS total_uses_remaining,
     336    COALESCE(SUM(upp.total_uses - upp.remaining_uses), 0) AS total_uses_used
     337  FROM userpackagepurchase upp
     338  GROUP BY upp.package_id
     339),
     340package_payment_stats AS (
     341  SELECT
     342    upp.package_id,
     343    COUNT(*) FILTER (WHERE p.status = 'PAID') AS paid_payments_cnt,
     344    COUNT(*) FILTER (WHERE p.status = 'PENDING') AS pending_payments_cnt,
     345    COUNT(*) FILTER (WHERE p.status = 'FAILED') AS failed_payments_cnt,
     346    COUNT(*) FILTER (WHERE p.status = 'REFUNDED') AS refunded_payments_cnt,
     347    COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'PAID'), 0)::numeric(12,2) AS paid_amount_total
     348  FROM payment p
     349  JOIN userpackagepurchase upp ON upp.purchase_id = p.package_purchase_id
     350  WHERE p.appointment_id IS NULL
     351  GROUP BY upp.package_id
     352),
     353usage_ops AS (
     354  SELECT
     355    upp.package_id,
     356    COUNT(apu.*) AS linked_appointment_usages,
     357    COUNT(*) FILTER (WHERE apu.finalized_at IS NOT NULL) AS finalized_usages
     358  FROM userpackagepurchase upp
     359  LEFT JOIN appointmentpackageusage apu ON apu.purchase_id = upp.purchase_id
     360  GROUP BY upp.package_id
     361)
     362SELECT
     363  pb.package_id,
     364  pb.package_name,
     365  pb.max_usage,
     366  pb.total_price,
     367  pb.services_in_package,
     368  pb.computed_services_price_sum,
     369  COALESCE(ps.purchases_cnt, 0) AS purchases_cnt,
     370  COALESCE(ps.active_purchases_cnt, 0) AS active_purchases_cnt,
     371  COALESCE(ps.expired_purchases_cnt, 0) AS expired_purchases_cnt,
     372  COALESCE(ps.cancelled_purchases_cnt, 0) AS cancelled_purchases_cnt,
     373  COALESCE(ps.total_uses_sold, 0) AS total_uses_sold,
     374  COALESCE(ps.total_uses_used, 0) AS total_uses_used,
     375  COALESCE(ps.total_uses_remaining, 0) AS total_uses_remaining,
     376  ROUND(100.0 * COALESCE(ps.total_uses_used, 0) / NULLIF(COALESCE(ps.total_uses_sold, 0), 0), 2) AS usage_rate_pct,
     377  COALESCE(pps.paid_payments_cnt, 0) AS paid_payments_cnt,
     378  COALESCE(pps.pending_payments_cnt, 0) AS pending_payments_cnt,
     379  COALESCE(pps.failed_payments_cnt, 0) AS failed_payments_cnt,
     380  COALESCE(pps.refunded_payments_cnt, 0) AS refunded_payments_cnt,
     381  COALESCE(pps.paid_amount_total, 0)::numeric(12,2) AS paid_amount_total,
     382  ROUND(COALESCE(pps.paid_amount_total, 0) / NULLIF(COALESCE(ps.purchases_cnt, 0), 0), 2) AS avg_paid_per_purchase,
     383  COALESCE(uo.linked_appointment_usages, 0) AS linked_appointment_usages,
     384  COALESCE(uo.finalized_usages, 0) AS finalized_usages,
     385  DENSE_RANK() OVER (
     386    ORDER BY COALESCE(ps.purchases_cnt, 0) DESC, COALESCE(pps.paid_amount_total, 0) DESC
     387  ) AS package_popularity_rank
     388FROM package_base pb
     389LEFT JOIN purchase_stats ps ON ps.package_id = pb.package_id
     390LEFT JOIN package_payment_stats pps ON pps.package_id = pb.package_id
     391LEFT JOIN usage_ops uo ON uo.package_id = pb.package_id
     392ORDER BY package_popularity_rank, pb.package_id;
     393
     394}}}
     395
     396=== Релациона алгебра
     397
     398{{{
     399PB ← γ_{package_id, name; svc_cnt:=COUNT(service_id), svc_sum:=Σ(discounted_price)}(Package ⟕ PackageService)
     400PS ← γ_{package_id; purchases:=COUNT(*), active:=COUNT_{status='ACTIVE'}(*), uses_sold:=Σ(total_uses), uses_used:=Σ(total_uses-remaining_uses)}(UserPackagePurchase)
     401PP ← γ_{package_id; paid_cnt:=COUNT_{status='PAID'}(*), paid_sum:=Σ_{status='PAID'}(amount)}(σ_{appointment_id=NULL}(Payment ⨝ UserPackagePurchase))
     402UO ← γ_{package_id; linked:=COUNT(*), finalized:=COUNT_{finalized_at≠NULL}(*)}(UserPackagePurchase ⟕ AppointmentPackageUsage)
     403Result ← PB ⟕ PS ⟕ PP ⟕ UO
     404}}}
     405
     406Се гледа кои пакети навистина се продаваат и трошат, дали остануваат неискористени, и дали цената на пакет носи очекуван приход.