From 5f8726ee4e3ea71abbf20bfeec5baa9f477bea34 Mon Sep 17 00:00:00 2001 From: Ryan Moon Date: Fri, 27 Mar 2026 14:51:23 -0500 Subject: [PATCH] Add planning documents for Forte music store platform 17 domain design docs covering architecture, accounts, inventory, rentals, lessons, repairs, POS, payments, batch repairs, delivery, billing, accounting, deployment, licensing, installer, and backend tech architecture. Plus implementation roadmap (doc 18) and personnel management (doc 19). Key design decisions documented: - company/location model (multi-tenant + multi-location) - member entity (renamed from student to support multiple adults) - Stripe vs Global Payments billing ownership differences - User/location/terminal licensing model - Valkey 8 instead of Redis --- planning/01_Overall_Architecture.docx | Bin 0 -> 13289 bytes planning/01_Overall_Architecture.md | 267 ++++ planning/02_Domain_Accounts_Customers.docx | Bin 0 -> 12493 bytes planning/02_Domain_Accounts_Customers.md | 359 +++++ planning/03_Domain_Inventory.docx | Bin 0 -> 17851 bytes planning/03_Domain_Inventory.md | 927 +++++++++++++ planning/04_Domain_Rentals.docx | Bin 0 -> 11819 bytes planning/04_Domain_Rentals.md | 235 ++++ planning/05_Domain_Lessons.docx | Bin 0 -> 19194 bytes planning/05_Domain_Lessons.md | 827 +++++++++++ planning/06_Domain_Repairs.docx | Bin 0 -> 13914 bytes planning/06_Domain_Repairs.md | 435 ++++++ planning/07_Domain_Sales_POS.docx | Bin 0 -> 11664 bytes planning/07_Domain_Sales_POS.md | 231 ++++ planning/08_Domain_Payments_Billing.docx | Bin 0 -> 11281 bytes planning/08_Domain_Payments_Billing.md | 199 +++ planning/09_Domain_Batch_Repairs.docx | Bin 0 -> 12332 bytes planning/09_Domain_Batch_Repairs.md | 301 ++++ .../10_Domain_Delivery_Chain_of_Custody.docx | Bin 0 -> 12928 bytes .../10_Domain_Delivery_Chain_of_Custody.md | 327 +++++ .../11_Domain_Billing_Date_Management.docx | Bin 0 -> 14901 bytes planning/11_Domain_Billing_Date_Management.md | 403 ++++++ .../12_Domain_Accounting_Journal_Entries.docx | Bin 0 -> 21104 bytes .../12_Domain_Accounting_Journal_Entries.md | 1219 +++++++++++++++++ ...Deployment_Compliance_Pricing_Support.docx | Bin 0 -> 21984 bytes ...3_Deployment_Compliance_Pricing_Support.md | 879 ++++++++++++ ...sted_Installer_Platform_Compatibility.docx | Bin 0 -> 20291 bytes ...Hosted_Installer_Platform_Compatibility.md | 653 +++++++++ planning/15_Licensing_Modules_Pricing.docx | Bin 0 -> 19614 bytes planning/15_Licensing_Modules_Pricing.md | 823 +++++++++++ planning/16_Windows_Installer_PowerShell.docx | Bin 0 -> 18829 bytes planning/16_Windows_Installer_PowerShell.md | 289 ++++ .../17_Backend_Technical_Architecture.docx | Bin 0 -> 19502 bytes planning/17_Backend_Technical_Architecture.md | 297 ++++ planning/18_Implementation_Roadmap.md | 624 +++++++++ planning/19_Domain_Personnel.md | 263 ++++ 36 files changed, 9558 insertions(+) create mode 100644 planning/01_Overall_Architecture.docx create mode 100644 planning/01_Overall_Architecture.md create mode 100644 planning/02_Domain_Accounts_Customers.docx create mode 100644 planning/02_Domain_Accounts_Customers.md create mode 100644 planning/03_Domain_Inventory.docx create mode 100644 planning/03_Domain_Inventory.md create mode 100644 planning/04_Domain_Rentals.docx create mode 100644 planning/04_Domain_Rentals.md create mode 100644 planning/05_Domain_Lessons.docx create mode 100644 planning/05_Domain_Lessons.md create mode 100644 planning/06_Domain_Repairs.docx create mode 100644 planning/06_Domain_Repairs.md create mode 100644 planning/07_Domain_Sales_POS.docx create mode 100644 planning/07_Domain_Sales_POS.md create mode 100644 planning/08_Domain_Payments_Billing.docx create mode 100644 planning/08_Domain_Payments_Billing.md create mode 100644 planning/09_Domain_Batch_Repairs.docx create mode 100644 planning/09_Domain_Batch_Repairs.md create mode 100644 planning/10_Domain_Delivery_Chain_of_Custody.docx create mode 100644 planning/10_Domain_Delivery_Chain_of_Custody.md create mode 100644 planning/11_Domain_Billing_Date_Management.docx create mode 100644 planning/11_Domain_Billing_Date_Management.md create mode 100644 planning/12_Domain_Accounting_Journal_Entries.docx create mode 100644 planning/12_Domain_Accounting_Journal_Entries.md create mode 100644 planning/13_Deployment_Compliance_Pricing_Support.docx create mode 100644 planning/13_Deployment_Compliance_Pricing_Support.md create mode 100644 planning/14_Self_Hosted_Installer_Platform_Compatibility.docx create mode 100644 planning/14_Self_Hosted_Installer_Platform_Compatibility.md create mode 100644 planning/15_Licensing_Modules_Pricing.docx create mode 100644 planning/15_Licensing_Modules_Pricing.md create mode 100644 planning/16_Windows_Installer_PowerShell.docx create mode 100644 planning/16_Windows_Installer_PowerShell.md create mode 100644 planning/17_Backend_Technical_Architecture.docx create mode 100644 planning/17_Backend_Technical_Architecture.md create mode 100644 planning/18_Implementation_Roadmap.md create mode 100644 planning/19_Domain_Personnel.md diff --git a/planning/01_Overall_Architecture.docx b/planning/01_Overall_Architecture.docx new file mode 100644 index 0000000000000000000000000000000000000000..e81bac25557fec0c36bfd2780cd874111d964e5a GIT binary patch literal 13289 zcmc(FWmKHY((VAkJ-E9|7~F!pI{`v)cMlTW-Q5BNmteu&o#5^s+#$#%d++QdeCOPC z@2{`ddUQ;+hOYU4oSkWBF5IWjly`3ohMP zHyUTQXT3Ek7c^4AYeJYJ7;oIX`A@xiUqd6G14R7#Ia_CTi`jy8cU@Ow-Bn))8fcdJ z%D%q-L@{3-{av$RrI>^BI-=&FUcRGp5)w>IRV9$G2U@^hsTnf*F0V8T5o3D#m1Jp; z&#X^`(;XSRBuC(UG*Vi%Z~Pcj>L=VzO>wB!OT>=i4Wz>mLy*9=#c}4;SlsqPQtvNY z4RM5n5(ujw&9Nh~t1}3tJH(TTI=i%%-9UZ{_ivdUlcK%|LInWcV}byXf6DB4X&q>4 zD3WkgJkY@4v+l4rYulOlYa06G>clgR4pk)`^gyeTH8AH(utdYG2Yq0dJ#*7tziYoC zqfjgi0U5^>EGy!F4XiCyQ`l3aj#DsvI5?UPB2|5h^KtCH`kj$p*l}!uSkC%=b;hgk z`3AGjH_0MOg|SCv(sgRzzh8Nz>n6Eo7X3>CLH;CRejE3j>@? z>`OC#X+hUWIdEw3@8bTL8v~UaCr>i3L%m!0Rg7UNjS|n{J5Ce4Q>;l>El@66F!`Ol zVzlI;f+Cje>Xz89uR8nWx~0H{L3*{9i%1D34AnlWvDJR8{=|cN>3Ea3Py;nmbHz5S z(p{|8j&SogEoUw3o)6_kb-s-G6P?6GMdh?CwO9#4rOiq~MM6{3qw_B6IE~Zxpu8+? z!odVL2`_gg9}I$(@I9mMNJXB3ua*&Jh0#m=X+%JGQX?pzTq$8 zE-DDhljjnDMWa5mO~7AdyfIstl!s=BG+w;tweJGd1wV{jQc37i(s0)t-+7(f>v+|0 zc&I6GPX@&g{=KOohm}CuG%w^+y`V8a?K1I;rw=Cla?82QdA3{w@_Wml&K)@vC4AC~lPA{X5WE;Au+e3a*fzFQZ8 zzCl@w#MGj;%Hz(#WkcHh;9P1)@laRd1v0Z0H5ryv*d{c`C@p$CQ;OMxG?S?l*ki*` z{sXSa1%}=P?>(Gf>kGd~4`k5#_1>isVo}q;uJUm>Xb`x8qrq)M*3XK40}A&(im4zM zDx+K(;Hpgs9Q9kYHEY2!{OPUw#`VmZ&75HA;6xbZ3^~cMA`1+3{tHMrc3HueB1X3u z@+3}2AkM-McNvc)vHEqxv8x9<^f_eRPO3$LTS>m&kv=g@749E}zk63+!+Ws>iIE_T zNA(UxEs8SND2ege2E1)es60ghN|lkJvM3T)Tdmyy=`ldkkvRgG%aAJ1C78+ z6kUr8g@K2pWm6%08=)htB$u}pBuyR(4+yfucdft_9zyVVDm9Z`IX1%rq97UC(KA2rm@7n^m6P}T$$10e&3VRpd` zlrKTp-nEcAg9bu!xi}TWu`zqsgDIl`N!Hdn6;?CzJz&AT9d}sb zl7wk+3uMWdOD zbi(hoHc;}+m&Pnd!9WF614s3kE1m5;n#T520!DZdEa|4uB`fVzE$&UtM^Qj$aNsVP zT5PN7c@VZr6u=FLGVT0!?6E;_R#@|22oq&is_k&6s~zUW57J&w&k0nUNK!i_8PPF* zR@YB`Yn>JtD{*>AYz);FJ*pi`5D&%U^qtN-8mZH|60jHNdjUn0~PzmRz?y3+#4|L@5QtO z4w4!Mz71V(wsxmwu%sT@0gw3zUsG!e5M_?=B~w_nX#$z2_WK4`K#kY9WnqIzExc`@Z;B7U_L373LPDk9370qN8#aRf*7DItW_7J1Nhy^5?x!uj!X#Mo@ABy zuu`ElBowQ7^P9+sea#$#{afKT(OI&Gv9#3hn!e)I9Je?x+N5{j7J$vfHi_qm!AVxO ze5qq9t2;6Y6O1!!W4URiFi2UbSE?B-!4dvG_^xS^zKP>tOruz{-g-4U{6^ZZZq)&6 z?+~LX9T)+o-x*SP?u$6@XZK!t zw?hMABOQ82!sBdH#r@47s}YSB<&9?98kgG6yU|yLTh&4Nr5v~0 z-?fQ69!{)mOGEKGXf}4%VlmlxxN!-He7_fzz0TJ)cg)NSV|M$XQ*tC+(43N%o{|H1 z=d$f)4}Nsx?=mvt6yxz-iS1_EtP0jWx#)bH)^}7<+Qmw%O#Ib~Q{46a?C1%TD^|-p zd0h0X=5VI%2p>YN1PFsSj5pMocR&K$G+S6EO>#T1N|l!#6BtJ23|CfYtdVeRB}3On z`iT3Mh^MT4uA8Ggz13q?RU2^bobY38d{t^luctxvMjL7~1x?>qtK(6HO7EA}j2;e- z=a(pOkl{9M14ChX2;@0|l|LI~aJqWr>9UIA|cD*Qu(s)yDiVj@Cb$WN_PCnP&SgS6qwgAB1_*rd3@J_k7?G4jC@0A?unaiMEB& zPkoZy_RFN4=l9MfM($$fm>%mQr$!M}{fQYmyHhd!`tq&Fb5!dE5ps&@BXW{4G5y$s z_|HE|$mJGYRdH@a?)4^3iud!IdK*Ui+-h6D5|67L=ekUa%PB_;mxgyRpG*EWQ+2k8 zf|v5;T?q&SyMOT*a3gWt)&Hv~7PRCYGF`?qlEGF8+7hoiE(;x~r7P!*0Qp8nuT zWvIMg(m-mI)w{eLruIhG-XOs<872~xV0?{Os_NxRvMY)WD{@_1&xAra_xmuwj|!Pn zhP&%W$rF9&W!>}i)w8s7Iab!@0;xn1AosdUF3n(tBLQ;%ILBfqu;z|w*qcRVAyUkE zxL9M=78B8%eWw$?`-YIn-Mom8V9T#8$QF1NN4R_sv>l! zw2&5zM!zy8ifm%jIK(@~ECE|~HYQGpKBm#QZILKAM)v$JY5w~8_~S~Sy_oxKZ(M{O zx5GewOV>*6rR&J8J|xryfr@DHK}E)#T{6o{)b0(Qw;@~d!z+%T#C+_B9U?nHzJ+zW z=uq(}ne@+MHAue}SdcQ3Z zrp31aLR}%xg+Mbr&i1h(x$tbglG8zUSq<%DxHb9lHr4_xtGoWtkI+|T_r%0&6^qC& z*K$VZTPbI*>HkC(`6x%j?Rj_~%Nq~Ewd z3OF*E4hro;eHRtz@#d&zsl9zg=bB@$Q=v^OJYKfdvIz>I7#iW{4Jn3JNYU$d*fh3^ zY}vkGX0jOrrj}r!Y#G<@1b@;n;FWtk_eelRRHwN~f7% z>O&{szPOS|UDC2`*@y$YgImrni-&`1aX8Fa|k_F-g>N4c>*JCy8B= zQC5L*jJgf!jd9CCq!SzCK!?724s$7qsuh3PK&mp00~J4C)2Md}LS3EhwjMGXEZNMm zVfz}Kog~sFabKe#)Z3yd;mmoVKn_~Ub6gDm9u~aO%>2WMPWD$7qI`=irunRGt) zfSSN84m(_peoU_uw1|$#j;eG)Xe2xIyUGc{T={Op*O-kbaXj|IB#@dtsx@#W9t0%4 zR&v~;>(qVa(o3>zcK2^tD|FpL?Y^>v z(7D~G8-B2$=I5)Z^~}^LwDfNs#-Rl9<6fWPzVI6!p9s^^!~6h`AZ+B*CYz^^_Xzja zoW#~X%w8w>jR7VAw~kVBCgf3o$>71XF_yBS3In9lCVEA%8*!;>?`pVqDG9`g2uebf zC6Yr04ZIyj`;(!G_n-}loy(^UWjWD|rh#*#QP)Y;$J`tzHIDtxS_=+OsBQ0uNte)F ze2T7L)m|+oP-z>6V7X#zS8koKu>Scavln1Dm#?1S>D_0@fBNKQ3Ec%t#(m(pRg?n* zS2Ju3(U4h7k{16}D;%M_uWoW$kMwQ#Sfu^_G(7|S#oy`DLXwvUOe!2B0&?4#Q6 ztsvqE#TE?tU!Il54~xT<~^XJ(MF>%NN7ayoVL}$i!uLY zKD04CrR}2KI{wOZ`;0ao)RdQ@o-0@W3;qRG*l1;`5TS*PnWwKH*)*Ly6ib&n)le=d z-ZFB&Ap7xk@$-*z?eYWEna8JbRoCMx-vWKvpxo+J^8_y>iy3ZZU+NB%2U zG7`9NLGomA=||U=?3LhrJwD5l;-I$ydL+13N4I+UkZvfoAFC#n-i&mb!)5uiC#h17 zVArhq^x5n46^Nq0I0$zFszGP;=2u#7)Hag?M2!XO8 z@xPy^`UodQyqm_A4s@!|C6b3e>ao@y_7%I0?^=cH@+264fxHVx72*{s$y%-3nm)b( zHO3ACDr@cA)oyDsKcX{3Pst1i6jDXa1rif~Z|;&KMW$M1N?s0tVKVlcP==FX;mJdgwH(gO=CP}T*e^q_Yyqh6LHtxHUzm{As#mz? zCb-1iV=EHZ0*}BDfcS}m{$AYfO95^cUH}Zi@U~@1c&X_jI+A9S z%JViAj?|ntbWU-)Bh=yoLBhn6lpt_YSYn3Mk%JC6aD$?>#IeW}gd#U}l4j_`FRW4B zDC)LT#uR#FMvL+*AF++RHO#OyzV6>DCri#_579PiQY{c>*7p~D9!2OdiGj{RHt^$_ zrdTqlDb~*u>620Gi;%O&p{j?>-I>bY`SMXWFG_cNoRU-TXfH?g8qSEi=467eEf%4jmwyg9-a!{jgNU>Wb3O#Yo=&~x>j`d?`>zdE$Pr7 z!pd?OZnCHxT9lXRnj?S>lj5QVTENhMpl#o%D}29Nz@@2`D;&5tr)4R1^136o4^p}L z>RidS7Y=e~8!n0>Xz~q|y*ArZD zD&{63oT9+yHh$0Ec&T2aa!ir;>Z#p|RA2#c6uKEpy|cXi)K=v5u}&K01$E2Kh4EWifgqt2HJe{k8f*qmcgjb9XbjW#i$ zHpGT$8c^OpJiIv(yE7Ghz%*_r?OoFVPser_GL&04eW@<hq1#-f6S1-|OTr6g*u8+JgMAbG;W_NG81{DU= zSbWs;!uSs=jlPltaXkd#YNjdu&-`?Ii9WgRCUjZ5vZR$yGh1gC)XBp+DZ(|#!(G94 z9h+ZNj<{ubtZjP$epVU&XO#!~X}3NV802iNte+QfE?Gc7vw{EsKA?cViV^=#)3dTQ z__3`&MQfV1GoboEO7wk5sRGCQ6!sauj2tf4e4n0F6yA(SjIrp^-DI+GXOTYUs;|RU zMq}i%P}*^&k-47VSws#r8Hea1m%nI}`-4Cbq7>{DHatA1rzn%UorcZ&$|4plz1Xq% zxv`ebe5M7|m-d-6I?Am|u^r6PFh$zV?0M~hYs7f~Jyeo5xEC}!=AmB!Uf50!s+-cB z)-(cW8?Y*q4>ID6wg-dNQ}JQdhp3J?4h|9)@v9yCnCmOi&)@2cX&{*|M^2{K4)ict z;_8=CA%)D^XbwHW*LNiHlBtkbn(Oy5W*7<3ZN|-@*KVx47L8sXYEyhK6FT$4jJPn_ z$;J_SIiS+>b-2&y=CnSQL_JbBI+~G=TvC8qLB%;qy6?Q9%yGR|Y_g!UrPv4>FM0Wa z{T%V9F8;3gC;|o9jx?Ecs#+t zmsRizwL6IUt^oBc3*tMkoSgU5A21eK=CeZZiz%q6)K2p(Ob6gsij8dzmt1je{inJn z@v`4}#gThc5boTxc5tI%t)9JbOI=6}^UQ-^!^-kre?eOgp_+hUFlfix)nD&S8`fh- zkD9FTO%5VT6#}d*mj4BJ$gsoPer6C@J$<;3G8l1J8u{wI^jfl^gX&m@eRX?h3X%o=CoJekXzPXirJ%_SP0H_ez(`(3 z(%_d74y*Iila_*)_>&79bZ7*;4PiD)ObJG&mMVd0yYTM4V6D$mTh9Wa!k-gr+qs4E z-l=7(RmrdFz_up1QN!yrF1gd`Y+YU#!WF_PkMOk^7J(M@EeqoMWJpriw!+|Pjlnk@ z!IhdIv0)n~5-x~yHjh4lb`SEVNN{N(nMY$v?L!;zmxQx&}!(5r^>Qu<} z$Tp)}^2lc1QN;OM{KV#majE6|2p) zJ?HQ8Z}f-+7!WByi)^roYhFbt$K;j5j&u$WpO~SO&kzB*Vu_U%Ww6TLoM1%K-PQn` zAbOGDOo8g!n&Je5g}NN7g)Pq|c6!aXKD^@i5Q;ghf*{80w@Ma4!X1RsN5~;~l!;?G zQsqnbK|WC~8~BywuW~6;s}JZ-Wg$`-C%a8n!cx5FnvLpx)9QycT*Mp+DEpHr+{vhC zt!1-T>BfZBtq(y7+Wck$qT!(u~uV=(3v{`SsjWwjAW$zFX685%GMp zNZ3mCZSk4v>vE$OF~nCh(D0bFB;KC_69|5a?C*`A86N&A`RR0JhXMdlp2VRcXk}^t zR1?=ya<(?G)BLgXt5pVMIv9}d;wV)oYOVR8syAELp>S8g-nzzM>y{ZL)tPkR$6-YVvoBzeXEMe@Ch1+oA$?s@!R9wMIN2;9$4((Sn@+WF#5-wUe zWI;i?yXOfUp;Jthpc8Zy^q~>UaF}6`9Byd2w-J4xX~RIn3d?D!tlzOl&1N3eiub%= zk$1H~Z2Z_}d>t`@191RG9T=4`Kh=d^K2|$9#NE$#HD+rSwWBMk)^bS5LHuC?$qDj} z&SS+gt#>(wU zqAM&Sc9sxnz(kFW#`{qu?wHqDtUI4G@Q_Xvmjh}SCx`E;@CNNWT`?Xz-mwaRMU94o zH`Eb;=Yp}fu#3QDAP-Q`_Y~?#RmhKf$(7Bfuq9O&irXaa+%0~>&KaWmrASm_Pf6Lx zKvjGnakZyyx~S&-702iM#u%fPfc=6?_&=Ahc9%96^Thg(4T|*p5_GJs|K#6y-=YvU zYOD&!<{+ycv9AiAnTZHR($$s38`ApbdUd_}9v!`0Kvy<@v%K&VplX9#MxDohew7&9 z$Y+?=H**t#_Iqg)MQ~&VBsw_XlPU~o@q z+6Y_&!hQwe1hT(uruar{BI^Ebefx9ITEXG71*8V`MGZe~r;?}R=ihtO(8|i*(#rnt zAY?LjNM@Y@sYi?Kw8=pML|s^#!;^0TY4HPKojuI&pxm6|qg6{xr*u7SA1*wKg2|e6-Y2BkbAq^#RTLZoK{U~Zu91>IkZG1>P z{N5+dM$Y_uNaivH+ts6kuc|i?xRrGJ_!aZ1`@j!c;rMIX2`GfnB8ePJN>MHtg?f?bgES zuUc{Fq%esUC{RrS>t-g~%*~P=9k&1zS+cLD!nx@BstFe>OXUawSntE{Z0*NPmE4mP zKN;EQh)Usai(RvA@dp`>4SKaGCugr2PHTY`^MLLjR%~%|8t+TRBz#RDobTLwb&2t& zysxL=W}wsD%H9BRtJ9LWR&Vi=y zgvfB4lZZJ*)=hcBzoS@2zs4KoXG&}0kxuY|9%JsHsk~2gxkX`oWcE>1W>>!+()!bU z$DHnJES}8P_36QTdj9HL`ul?U!v_AkZKOl_5kQIHgv$i#or38@fgOXE2-@cWcojkf zKRI3QW?IdM#;b=$_cKeL1!goG>r`4ybggu8Z=aqITyM2Cw{VUo0zLwjSxAr_kX*}D zg)X9)c9p0H2+PJ8FMt}Y5)aZ|C;A6i^N)}#r(l2b$tw{a*7jtj(N9+TZ-0-0rT)K0 zIutu^xlV}Wuhq52(-vMo*&jR}g0Iq4l)MO;iP@9iO4xe2b>QR%HT*?6mX)mun3rVr zkOVBYdN7TvYIlatC7fj>3u3^PFmVLGIs2}260=}FrAaSkLI{_edRi{YkQ?{FIU>J9jt#mGAFXLm0^z`#vX`zShzVfHjn?SUgXvtL(6~KCS9}@T|&h{Jbars1cKmL9u)KxyE?K&mbJ!`bCYtTBI=R z%+hu91hsa|b-hsQnlhIL<=pvj<+@G3je zNFF-iC_C|vTmxA)?*J+!YaIj+b$sVsz7SU`G8YR!@nF7c{3zoh;8s>iJN^3*@1Oeq z-7cT?!2PcE|829B&gk#|_+*xLPd$JCnx&nAz5P=N@>c@qQ|#hX3gXinF+d5ik4enqSwLr5X zVe7AxCUmKrrKQz!Zr1##A&tFGTm-3dz?VB*+K5pHHkYr5oc&OXH9tr0FflI(%-FjN z+arqenq`m2&)iIg+Ms;{jd$(b(>9i(q7eLUFbvhRBSUp1W3Qq!M|GMB=WLCsagbo< z7BAA~=zmV2nN{PDr!MElQ3iPh0-=t+0=(Re)gRip)6eiY$6U=ohH?94+`*Etx{#gdd6okHo2P%&W80t!HKz;?L39^`y`4C*Sg4 zed<|R{OGy-Tb~n)wrhl#5Gxus?5fdk6xqhBDAEqj-_OHldbTs)!DW7J&{7~1Pe_yT z7TqIVj!Bqq?HCtl4k2NQr&059i}M1HdxK1W#q!?cd((oqpM%)T2}*G@>s)m1*F05d zDgtu5`|V(3D%VP2TdAdlRf+Om`x4gXvC3REr&j{i^EOgM!2AnV+KM{_UH{N*icy>! zbA2$6HVZg3Xu0$@;)sF^^Fm+ELa2I4Z?{6Rxg7CFwYN#+Xyx`94{PHa9 zm};8a3HNRoP4$CXyzRb86%PZwmF)qRTplI3*mKwPu~ngF^V3O%HzBlyJsIdWsM57% zlQ*3QW7LB6%@t)9x5$pb?RsD?|KP-@V_I5)m;T<%`%3dOW zTAUF3clpvIXudooSIr)=?WewwAdl8&zVUsP50<_ zU zFgB=y{AS)5#61A>pxsUbGgYm;g*S;68u8)Xi}mQhbnN-DOwD&+=`rnbHw>y@4Gdz#atx`HorOz84E!~c)l^P5QjHv|UQ&MopW?JX+t8m=dftxxi<0M)3ID~y?=+snpI1eG!P(LOf&Z$J`~v^a zl**4J?cd|a^0%;l3o(De{$Bj^r0q{20058u8|-Ha_c{D|I`S9X7Uv(9|42^$!v9aM zpM2bZ#t*L9@qea#eqsJ*`FUdKCkg;~`!D7%3w{oKoJ-o&d77m=NG(( z>K{gazYU+m|NA-`X#QBo??nFNI-cKizZlV_{fCm_Q^N1xTkrqI$)CC{qx*xCKdAh} W3iw}J4;_FGaC`FTRq22H`2PV6tMy6% literal 0 HcmV?d00001 diff --git a/planning/01_Overall_Architecture.md b/planning/01_Overall_Architecture.md new file mode 100644 index 0000000..e938de6 --- /dev/null +++ b/planning/01_Overall_Architecture.md @@ -0,0 +1,267 @@ +Music Store Management Platform + +Overall System Architecture + +Version 1.0 | Draft + + + +# 1. Executive Summary + +This document describes the overall architecture of the Music Store Management Platform — a purpose-built system to replace AIM (Tri-Tech) for independent music retailers. The platform handles point-of-sale, inventory, rentals, lessons, repairs, customer management, financial reporting, and payment processing through a modern cloud-native stack. + + + +# 2. System Overview + +## 2.1 Goals + +- Replace legacy AIM/MSSQL system with a modern, maintainable platform + +- Support in-store desktop operations and mobile field sales (conventions) + +- Provide customer-facing web portal for billing and repair status + +- Enable clean migration of historical AIM data + +- Support future multi-tenant expansion without full rewrite + + + +## 2.2 Application Inventory + +App + +Technology + +Purpose + +Desktop + +Electron + React + +Full store management — POS, inventory, repairs, rentals, lessons, reporting + +Mobile (iOS) + +React Native + +Field sales at conventions — POS, customer lookup, Stripe Terminal BT + +Customer Portal + +React (web) + +Self-service — lesson billing, repair status, invoice history + +Admin Panel + +React (web) + +Internal ops — users, settings, audit logs, migration tools, no payments + +Backend API + +Node.js / Python + +REST API on AWS EKS — all business logic, Stripe integration, PDF generation + + + +## 2.3 Monorepo Structure + +/packages /shared ← TypeScript types, API clients, business logic, pricing engine /desktop ← Electron app (Windows/Mac/Linux) /mobile ← React Native iOS app /web-portal ← Customer-facing React web app /admin ← Internal admin React web app /backend ← Node.js/Python API server /migration ← AIM MSSQL → Postgres ETL scripts + + + +# 3. Infrastructure + +## 3.1 AWS Services + +Service + +Usage + +EKS + +Kubernetes cluster hosting API, admin panel, customer portal + +Aurora PostgreSQL + +Primary database — all domain data, append-only event sourcing pattern + +ElastiCache (Redis) + +Session cache, job queues, rate limiting + +S3 + +PDF storage (invoices, reports), database backup archival, static assets + +Secrets Manager + +Stripe API keys, DB credentials, JWT secrets + +CloudWatch + +Logging, metrics, alerting + + + +## 3.2 Database Backup Strategy + +- Aurora automated continuous backup — point-in-time recovery up to 35 days + +- Scheduled daily snapshots exported to S3 (separate bucket, cross-region) + +- Long-term archival snapshots retained 1 year for financial compliance + +- Recovery runbook documented and tested quarterly + +- RTO target: 2 hours | RPO target: 1 hour + + + +# 4. Payment Architecture + +## 4.1 Stripe Integration + +Stripe is the sole payment processor. All payment flows are handled through Stripe. Card data never touches application servers. + + + +Feature + +Usage + +Stripe Terminal + +In-person payments — WiFi reader (desktop), Bluetooth reader (iOS/mobile) + +Stripe Elements + +Keyed card entry on desktop — PCI-safe, card data goes direct to Stripe + +Subscriptions + +Recurring billing for lessons and rentals — monthly, with line items per enrollment + +Webhooks + +Async payment events — invoice.paid, payment_failed, subscription updates + +Card Updater + +Automatic card number updates when banks reissue — reduces billing failures + +Stripe Connect + +Architecture-ready for future multi-tenant expansion (not built initially) + + + +## 4.2 Billing Model + +- Accounts are the billing entity — one Stripe customer per account + +- Students and rentals link to an account — family billing supported + +- Subscriptions support multiple line items — consolidated billing option + +- Split billing supported — separate subscriptions per enrollment if preferred + +- Billing groups control consolidation — enrollments with same group_id share a subscription + + + +# 5. Authentication & Authorization + +- Auth provider: Clerk or Auth0 — handles user management, MFA, SSO + +- Role-based access control: Admin, Manager, Technician, Instructor, Staff + +- Customer portal uses separate auth flow (email/password or magic link) + +- All API routes require JWT — validated on every request + +- Sensitive operations (large discounts, refunds, write-offs) require manager role + + + +# 6. Cross-Cutting Concerns + +## 6.1 Audit Trail + +Every discount, adjustment, price override, refund, and write-off is logged with: who, when, original value, new value, reason code, and whether approval was required. Audit records are append-only and cannot be deleted. + + + +## 6.2 Multi-Tenant Readiness + +All domain tables include a company_id column from day one — this is the tenant-scoping key. Tables dealing with physical inventory, transactions, cash drawers, and delivery also include a location_id column to identify the specific physical location. Business logic enforces company_id scoping on all queries. Stripe Connect architecture is documented but not implemented initially. Adding a second tenant is a configuration and billing exercise, not a rewrite. + + + +## 6.3 Offline Support + +The iOS mobile app supports offline mode for convention use. Transactions are queued locally and synced when connectivity is restored. The desktop app assumes reliable store network connectivity. + + + +# 7. AIM Migration Strategy + +## 7.1 Source System + +AIM by Tri-Tech — Windows desktop POS running against a MSSQL (SQL Server Express) database on the store's local network. The application runs from a network share. Migration connects directly to MSSQL — AIM application is not involved. + + + +## 7.2 Migration Phases + +Phase + +Scope + +Notes + +1 + +Schema exploration + +Connect to AIM MSSQL, map tables to new domain model + +2 + +ETL development + +Python scripts: read AIM → transform → load Postgres staging + +3 + +Data validation + +Dry runs, exception logging, manual review of edge cases + +4 + +Parallel operation + +New system live for new customers, AIM continues for existing + +5 + +Customer migration + +Re-enroll active rentals/lessons in Stripe at renewal points + +6 + +AIM wind-down + +All recurring billing migrated, AIM read-only, close after chargeback window + + + +## 7.3 Legacy Data Tagging + +All migrated records include: legacy_id (AIM's original ID), legacy_source ('aim'), migrated_at timestamp, and requires_payment_update flag for records still on old processor. \ No newline at end of file diff --git a/planning/02_Domain_Accounts_Customers.docx b/planning/02_Domain_Accounts_Customers.docx new file mode 100644 index 0000000000000000000000000000000000000000..969ea414a454b0efdde032dacc8e5e775c452e5c GIT binary patch literal 12493 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 SdT0PNfXiz}Pr>*0<^K2orJT` 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 O#!v%n(Py}VEC&E`h!E)j literal 0 HcmV?d00001 diff --git a/planning/05_Domain_Lessons.md b/planning/05_Domain_Lessons.md new file mode 100644 index 0000000..e149b9f --- /dev/null +++ b/planning/05_Domain_Lessons.md @@ -0,0 +1,827 @@ +Music Store Management Platform + +Domain Design: Lessons + +Version 1.2 | Updated: Session notes, grading scales, lesson plans, parent portal + + + +# 1. Overview + +The Lessons domain manages instructor scheduling, student enrollments, attendance tracking, and recurring lesson billing. Version 1.2 adds a complete teaching toolkit: per-session notes, custom grading scales, structured lesson plans with curriculum tracking, and homework assignment. These features turn the lessons module from a billing tool into a genuine teaching platform that instructors and parents will actively use. + + + +# 2. Feature Summary + +Feature + +Description + +Visible To + +Session notes — internal + +Instructor private notes per lesson. Not shared with student or parent. + +Instructor, Admin + +Session notes — student + +Shareable lesson summary — what was covered, progress comments. + +Instructor, Student, Parent + +Homework assignment + +Practice instructions assigned after each lesson. + +Instructor, Student, Parent + +Topics covered + +Tags for what was worked on — links to lesson plan items. + +Instructor, Admin + +Custom grading scales + +Store or instructor-defined scales. Letter, numeric, descriptive, or music-specific. + +Instructor, Admin + +Lesson plans + +Structured curriculum per student. Sections, items, status, grades. + +Instructor, Student, Parent + +Progress tracking + +Mastered / in progress / not started per curriculum item. History of grades. + +Instructor, Student, Parent + +Parent portal + +Parents see plan progress, session summaries, homework, and grade history. + +Parent via portal + + + +# 3. Core Entities + +## 3.1 instructor + +id, company_id, employee_id (FK), display_name, bio,instruments (text[]), is_active, created_at + + + +## 3.2 lesson_type + +id, company_id, name, instrument, duration_minutes,lesson_format (private|group), base_rate_monthly, created_at + + + +## 3.3 schedule_slot + +id, company_id, instructor_id, lesson_type_id,day_of_week, start_time, room, max_students,is_active, created_at + + + +## 3.4 enrollment + +Column + +Type + +Notes + +id + +uuid PK + + + +company_id + +uuid FK + + + +member_id + +uuid FK + +The member taking lessons + +account_id + +uuid FK + +The billing account + +schedule_slot_id + +uuid FK + + + +instructor_id + +uuid FK + +Denormalized for query convenience + +status + +enum + +active | paused | cancelled | completed + +start_date + +date + + + +end_date + +date + +Null for open-ended + +monthly_rate + +numeric(10,2) + +Actual rate + +billing_group + +varchar + +Consolidation group + +stripe_subscription_id + +varchar + + + +stripe_subscription_item_id + +varchar + +Line item if consolidated + +makeup_credits + +integer + +Available makeup credits + +active_lesson_plan_id + +uuid FK + +Current lesson plan for this enrollment + +notes + +text + +Enrollment-level notes + +legacy_id + +varchar + +AIM enrollment ID + +created_at + +timestamptz + + + + + +## 3.5 lesson_session + +Column + +Type + +Notes + +id + +uuid PK + + + +enrollment_id + +uuid FK + + + +company_id + +uuid FK + + + +scheduled_date + +date + + + +scheduled_time + +time + + + +actual_start_time + +time + +Nullable — if late start noted + +actual_end_time + +time + +Nullable — if early end noted + +status + +enum + +scheduled | attended | missed | makeup | cancelled + +instructor_notes + +text + +Internal only — not visible to student or parent + +member_notes + +text + +Shareable lesson summary + +homework_assigned + +text + +Practice instructions for next session + +next_lesson_goals + +text + +What to focus on next lesson + +topics_covered + +text[] + +Free tags: scales, sight-reading, repertoire, theory + +makeup_for_session_id + +uuid FK + +Self-ref — if this is a makeup session + +notes_completed_at + +timestamptz + +When instructor finished post-lesson notes + +created_at + +timestamptz + + + + + +# 4. Custom Grading Scales + +Every store can define their own grading scales. Scales can be assigned at the store level (default for all instructors) or at the instructor level (personal preference). Individual lesson plan items can reference any available scale. + + + +## 4.1 grading_scale + +Column + +Type + +Notes + +id + +uuid PK + + + +company_id + +uuid FK + + + +name + +varchar + +e.g. 'Standard Letter', 'ABRSM Style', 'Music Progress' + +description + +text + +Optional explanation of the scale + +is_default + +boolean + +Default scale used when none specified on plan item + +created_by + +uuid FK + +Instructor or admin who created + +is_active + +boolean + + + +created_at + +timestamptz + + + + + +## 4.2 grading_scale_level + +Column + +Type + +Notes + +id + +uuid PK + + + +grading_scale_id + +uuid FK + + + +value + +varchar + +The grade value: A, 4, Distinction, Mastered + +label + +varchar + +Longer description shown in UI + +numeric_value + +integer + +1-100 for averaging and reporting + +color_hex + +char(7) + +UI display color e.g. #4CAF50 for green + +sort_order + +integer + +Display order — highest grade first + + + +## 4.3 Seeded Default Scales + +Scale Name + +Levels + +Standard Letter + +A+ A A- B+ B B- C+ C D F + +Numeric 1-10 + +10 9 8 7 6 5 4 3 2 1 + +ABRSM Style + +Distinction Merit Pass Below Pass + +Progress + +Mastered Proficient Developing Beginning + +Concert Readiness + +Concert Ready Performance Ready Practice Ready Learning + +Simple + +Excellent Good Needs Work + + + +All default scales are seeded at store creation and can be modified or deactivated. Custom scales can be added at any time. Scale levels cannot be deleted if they have been used in a graded item — only deactivated. + + + +# 5. Lesson Plans + +A lesson plan is a structured curriculum for a specific student enrollment. It is organized into sections (e.g. Scales, Repertoire, Theory) with individual items in each section. Each item tracks status, grade, and progress history. Multiple plans can exist per student — one active at a time per enrollment. + + + +## 5.1 member_lesson_plan + +Column + +Type + +Notes + +id + +uuid PK + + + +company_id + +uuid FK + + + +member_id + +uuid FK + + + +enrollment_id + +uuid FK + + + +created_by + +uuid FK + +Instructor who created the plan + +title + +varchar + +e.g. 'Year 2 Piano', 'Grade 3 Violin' + +description + +text + +Goals and context for this plan + +template_id + +uuid FK + +Nullable — if created from a plan template + +is_active + +boolean + +Only one plan active per enrollment at a time + +started_date + +date + + + +completed_date + +date + +Nullable — set when all items mastered + +created_at + +timestamptz + + + +updated_at + +timestamptz + + + + + +## 5.2 lesson_plan_section + +id, lesson_plan_id (FK), title, description,sort_order, created_atExample sections: Scales & Arpeggios Repertoire Theory Technique Sight Reading Ear Training + + + +## 5.3 lesson_plan_item + +Column + +Type + +Notes + +id + +uuid PK + + + +section_id + +uuid FK + + + +title + +varchar + +e.g. 'C Major Scale 2 octaves', 'Fur Elise' + +description + +text + +Additional detail or context + +status + +enum + +not_started | in_progress | mastered | skipped + +grading_scale_id + +uuid FK + +Nullable — which scale to use for this item + +current_grade_value + +varchar + +Most recent grade assigned + +target_grade_value + +varchar + +Grade required to mark as mastered + +started_date + +date + +When first worked on + +mastered_date + +date + +When marked mastered + +notes + +text + +Instructor notes specific to this item + +sort_order + +integer + + + +created_at + +timestamptz + + + +updated_at + +timestamptz + + + + + +## 5.4 lesson_plan_item_grade_history + +Every grade assigned to a plan item is preserved. This builds a complete history of how a student progressed through each piece or skill over time. + +id, lesson_plan_item_id (FK),lesson_session_id (FK nullable), -- which session this grade was givengrade_value, -- the gradegrading_scale_id (FK),graded_by (uuid FK), -- instructornotes, -- context for this gradecreated_at + + + +## 5.5 lesson_session_plan_item + +Links lesson sessions to the plan items worked on during that session. Enables the full history of when each curriculum item was practiced. + +id, lesson_session_id (FK), lesson_plan_item_id (FK),was_worked_on (boolean),grade_value (varchar nullable), -- grade if assessed this sessiongrading_scale_id (FK nullable),notes (text nullable), -- session-specific notes on this itemcreated_at + + + +# 6. Lesson Plan Templates + +Instructors can save lesson plan structures as templates to quickly create plans for new students. A template defines the sections and items without any student-specific data. Templates can be shared across the store or kept private to an instructor. + + + +lesson_plan_template id, company_id, created_by (FK), title, instrument, skill_level (beginner|intermediate|advanced), description, is_shared (boolean), created_atlesson_plan_template_section id, template_id (FK), title, sort_orderlesson_plan_template_item id, section_id (FK), title, description, suggested_grading_scale_id (FK nullable), sort_order + + + +When creating a new lesson plan from a template, all sections and items are copied to the new plan. Changes to the plan after creation do not affect the template. + + + +# 7. Example — Year 2 Piano Plan + +Student: Emma Chen Instrument: PianoPlan: Year 2 Piano Status: Active Progress: 40%SCALES & ARPEGGIOS [done] C Major 2 octaves Mastered Oct 2024 [done] G Major 2 octaves Mastered Nov 2024 [>> ] D Major 2 octaves In Progress Grade: B [ ] A Major 2 octaves Not Started [ ] C Major arpeggio Not StartedREPERTOIRE [done] Minuet in G (Bach) Mastered Sep 2024 [>> ] Fur Elise (Beethoven) In Progress Grade: B+ [ ] Moonlight Sonata Mvt 1 Not Started [ ] Sonatina Op.36 No.1 Not StartedTHEORY [done] Key signatures to 2 sharps Mastered [>> ] Intervals - 3rds and 6ths In Progress [ ] Chord inversions Not StartedTECHNIQUE [>> ] Hanon exercises 1-10 In Progress [ ] Scales in thirds Not StartedLast lesson (Nov 15): Topics: Fur Elise bars 1-16, D Major scale Homework: Practice Fur Elise bars 1-8 hands together daily Notes for parent: Great focus today. Right hand is clean. Next goals: Clean up arpeggios in bars 5-6 + + + +# 8. Instructor Post-Lesson Workflow + +After each lesson the instructor completes a brief post-lesson record. The UI is optimized for speed — a mobile-friendly form the instructor fills in immediately after the student leaves. + + + +Post-Lesson Form — Emma Chen, Nov 15Topics covered today: [+ Add from plan] [x] Fur Elise (Beethoven) Grade: [B+ v] [x] D Major 2 octaves Grade: [B v] [+ Add custom topic]Homework assigned: Practice Fur Elise bars 1-8 hands together dailyNotes for parent/student: Great focus today. Right hand melody is clean. Left hand needs more practice on the arpeggios.Private instructor notes: Emma seems distracted lately - check in with parentNext lesson goals: Clean up bars 5-6 arpeggios. Start bars 17-24.[Save Notes] + + + +- Topics linked to plan items auto-update item status to in_progress if not_started + +- Grade entry creates a lesson_plan_item_grade_history record + +- Student notes and homework immediately visible in parent portal after save + +- Private instructor notes never visible outside staff views + +- Notes completed timestamp recorded — store can see if instructors are filling in notes + + + +# 9. Parent Portal — Lessons View + +Parents access the customer portal to see their child's lesson progress. The view is read-only and shows only student-facing content — never instructor private notes. + + + +Portal — Emma Chen's LessonsCurrent Enrollment: Piano with Ms. Sarah M. Tuesdays 4:00pm Next lesson: Nov 22Last Lesson Summary (Nov 15): What we worked on: Fur Elise, D Major Scale Homework: Practice Fur Elise bars 1-8 hands together daily Notes: Great focus today. Right hand melody is clean. Left hand arpeggios need more work.Lesson Plan Progress: 40% complete Scales: 2 of 5 mastered Repertoire: 1 of 4 mastered [In progress: Fur Elise] Theory: 1 of 3 mastered Technique: 0 of 2 mastered [View Full Plan]Recent Grades: Fur Elise B+ Nov 15 D Major Scale B Nov 15 Minuet in G A Sep 28 [Mastered]Upcoming Lessons: Nov 22 Tuesday 4:00pm Nov 29 Tuesday 4:00pmAttendance: 12 attended, 1 missed, 0 makeups available + + + +# 10. Business Rules + +## 10.1 Notes + +- instructor_notes are never exposed via any API route accessible to students or parents + +- member_notes and homework are visible in parent portal immediately after save + +- notes_completed_at tracked per session — admin can identify instructors not completing notes + +- Notes are optional — sessions can be marked attended without post-lesson notes + +- Notes can be edited after save — edit history not required but updated_at tracked + + + +## 10.2 Grading Scales + +- Each store gets all default scales seeded at creation + +- Default scale is used when a plan item has no explicit scale assigned + +- Scale levels used in grade history cannot be deleted — only deactivated + +- Numeric_value enables cross-scale average calculations in reports + +- Instructors can create personal scales visible only to them + + + +## 10.3 Lesson Plans + +- Only one plan can be active per enrollment at a time + +- Completing a plan (all items mastered) does not auto-create the next plan + +- Plan items marked mastered record mastered_date automatically + +- Skipped items excluded from progress percentage calculation + +- Plan history preserved when enrollment ends — never deleted + +- Templates are copied on use — template changes do not affect existing plans + +- Plan visible in parent portal — instructor controls what items appear there via is_shared flag on items + + + +## 10.4 Scheduling Constraints + +- Student cannot be enrolled in two slots at the same day and time + +- Instructor cannot teach two students simultaneously unless group lesson + +- Group lessons enforce max_students cap on schedule_slot + +- Makeup sessions linked to original missed session via makeup_for_session_id + + + +# 11. Reporting + +Report + +Description + +Student progress summary + +Plan completion % per student, items mastered, current grades + +Instructor notes compliance + +Which instructors are completing post-lesson notes and how quickly + +Grade distribution + +Grade spread per instructor, per instrument, per plan item + +Curriculum progress + +How many students are working on each plan item — popular pieces + +Homework completion + +Homework assigned vs next-session progress correlation + +Retention by progress + +Do students who have active lesson plans churn less? Correlation report. + +Plan template usage + +Which templates are used most, average completion time per template + +Attendance summary + +Attended, missed, makeup by student, instructor, and period \ No newline at end of file diff --git a/planning/06_Domain_Repairs.docx b/planning/06_Domain_Repairs.docx new file mode 100644 index 0000000000000000000000000000000000000000..15c36e2a7ec9bfb78e90ba26fc848cba67c0d26b GIT binary patch literal 13914 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 ClKYte literal 0 HcmV?d00001 diff --git a/planning/07_Domain_Sales_POS.md b/planning/07_Domain_Sales_POS.md new file mode 100644 index 0000000..ce6e6a8 --- /dev/null +++ b/planning/07_Domain_Sales_POS.md @@ -0,0 +1,231 @@ +Music Store Management Platform + +Domain Design: Sales & Point of Sale + + + +# 1. Overview + +The Sales & POS domain handles all in-person and counter transactions — retail sales, deposits, repair payments, rental fees, and account charges. It integrates with Stripe Terminal for card-present payments and Stripe Elements for keyed entry. + + + +# 2. Transaction Types + +Type + +Description + +sale + +Retail sale of product — instruments, accessories, supplies + +repair_payment + +Payment collected at repair ticket pickup + +rental_deposit + +Security deposit collected at rental start + +account_payment + +Payment against outstanding account balance + +refund + +Return of payment — full or partial + + + +# 3. Database Schema + +## 3.1 transaction + +Column + +Type + +Notes + +id + +uuid PK + + + +company_id + +uuid FK + +Tenant scoping + +location_id + +uuid FK + +Physical location where transaction occurred + +transaction_number + +varchar + +Human-readable ID + +account_id + +uuid FK + +Nullable for anonymous cash sales + +transaction_type + +enum + +sale|repair_payment|rental_deposit|account_payment|refund + +status + +enum + +pending|completed|voided|refunded + +subtotal + +numeric(10,2) + +Before tax and discounts + +discount_total + +numeric(10,2) + +Total discounts applied + +tax_total + +numeric(10,2) + + + +total + +numeric(10,2) + +Final amount charged + +payment_method + +enum + +card_present|card_keyed|cash|check|account_charge + +stripe_payment_intent_id + +varchar + +For card transactions + +stripe_terminal_reader_id + +varchar + +Which reader was used + +processed_by + +uuid FK + +Employee who processed + +notes + +text + + + +created_at + +timestamptz + + + + + +## 3.2 transaction_line_item + +id, transaction_id, product_id (nullable), inventory_unit_id (nullable),description, qty, unit_price, discount_amount, discount_reason,tax_rate, line_total, created_at + + + +## 3.3 discount + +id, company_id, location_id, name, discount_type (percent|fixed),discount_value, applies_to (order|line_item|category),requires_approval_above (threshold amount), is_active,valid_from, valid_until, created_at + + + +## 3.4 discount_audit + +id, transaction_id, transaction_line_item_id, discount_id,applied_by (employee_id), approved_by (employee_id, nullable),original_amount, discounted_amount, reason, created_at + + + +# 4. Discount & Adjustment Rules + +- Discounts are applied at line item or order level + +- Discounts exceeding manager threshold require approval — logged in discount_audit + +- Manual price overrides treated as discounts — require reason code + +- Promotional codes validated against discount table before application + +- All discounts appear as line items on invoice — original price shown + +- Discount audit records are immutable — no deletion permitted + + + +# 5. Payment Methods + +Method + +Integration + +Notes + +Card present + +Stripe Terminal WiFi + +Desktop — WisePOS E reader + +Card present (BT) + +Stripe Terminal BT + +iOS — Bluetooth reader at conventions + +Card keyed + +Stripe Elements + +Desktop — PCI safe, card data never touches app + +Cash + +Manual entry + +Change calculated, drawer tracked + +Check + +Manual entry + +Check number recorded + +Account charge + +Internal + +Charges to account balance — billed later \ No newline at end of file diff --git a/planning/08_Domain_Payments_Billing.docx b/planning/08_Domain_Payments_Billing.docx new file mode 100644 index 0000000000000000000000000000000000000000..c94d9ff26ab7b811dbb0fc07c84d1ab387eb0194 GIT binary patch literal 11281 zcmc(_bySsIw?4e-?(UQZ>5%U3?(Pl+q`SMMq`OPHyO9n-x{*de;)nY9sJ!nvtOU!?th#D^J|K(gRzz4 zzY`&U$m&axHNJhCKm-Z^;Ql?)$ky<+wXuy8or|><-FIkN+_DS^5n_ODfZcI*nMMTV zQ3S(YnL2@Ybq%jQ7Nk^!ha1BiY8JRt-(e1>v>Qf_()tV&1A^6VGx;-NWk>KrYfgig zH=5@T=L7Yrm()_B>q3|!7;5gm{AWG`+)zjt0Fi*9R~_?vrEH-Fdv0s-o~qoz#@ZGB zvfS5sVxK!Av;ss!`(LkT!3wL!$*6_jTqV9d?Elq~P} zTkwl=zI(wg$q~F1iy) zbHIx1iPnW73`44NNXlcrSUX^gbC^$VHhE;srx@RhFQFtp6d9&-IK$InkWndHEf^Ya z)GuyDUSRL$5-O4?6dA}w5Y*kiHurhO6gQ$sNorFxUB|h=&rC*WaCm=QtsBNT|-H>}#4{HQo;i&!FmI--v*I>-KW0Gr0$)ff+POpSPURxMls$ zJhV0a1+#(JG)JPo{#EMxIM|w&n-h&}bEx|Ic5I&qE+2yn#w~YpVRh!|`QbwCZ2}98 z{ILqYVai5r=1WEs8wIUtmgr9}MMu`gCYHj)Li6&!!bPkM30>v9%<&8IzqLg41-^1a z5I-uHvOvTt$4Asa9(MHiE`f+o1ij?S0}H?@6)k#IKOumcu6(}zkEj>+c=gx8EEkTsQ?(zo`UHOv?0>wP7o znmzgT0|%pPRUf5_dFr~wHtc>0Iol|oNZ%<|sEb8;z`Y~ws4&_)Z-a?VMDi5hjET=6 zqR^8HW}&Nbt=tV!QbW3sT=<5b(?k=QistV{Skm{SzIhNil);J%W2#*k4$`v;g;*@h zm0naCG+a_}k140Gcsi-wb1Pp-fMQHKer%rS#ynIWoVI<~^Zd-qpXv<5nO@U19fDy0-`(Nenr0b@rDZhnM4P+cR_TwB)@a?d9pIP5x0xC%vxz z0!og^EEZO2{=72wVs?X8tv(MG&AAJ8hV-8;O#)YK)pDj>(p8lPSiS~y0*O_ zo*3i*2|N7q@PJRrcAu`Zd<|527^t==CzO=09cF-Cf9^HbNL%Va>AC_z${txDkvzZExqrOOh;aR zm`WPBC?TO!4K5I9-xX9hc8ibqbo~W5c8>^rg^4u7u&D|#L^~lYycK=o%kwI|&0di! z6mS#zY67r#Se0>SMMn-`7=)RQH50B{7FFEY94u~j2Fz)i zBeTe5y4$MA59?ikEEu}?2#!qDL0*h`6;(YXAz`)aWG`!|UTheCnL3(2rrW@#jhuSb z4FZQxqp%tXKEY9ru15k)$hx6M!}if(g5dF{BB=56D>ZT~rtrv$m)6+EQ=e|&7@cX} zSQ_~W%qk7IGC|ShW`MDAuU4UXN-`XQ;ng`vRT3pC_ArkkMs8$=)Unu4V)qJTT8&{g zxzOIE^08pe=8LaRH6iS7FmpG>5kY!kt#M*er}Phr4!r_BFxsJN1~XAsrvFe(V}LVe zNp_n!sg^VZz5`5Nq#`-dFFGk&=Rw{XJ|)5g)lag;CSgNF$bg-!W&x$f&aglimKm8; z=&njyCz;+w3t4O(b=z=}yCiD0tbDcX%j=Kh zbbX(1uH{byF&$E|1mQQ2KzYtx9+!uGds&Ql8|_07^LJ^xQPoi{CgoY89=un)S9sA{ z`^QIsDj4dnpfuO%>#pQQf|r)3^vvf(Oy9L%i=nieQ1pDg{Gi2Ve_9!$J8#w}OVI)o z$j0a9m(}WmOZ9Hmdup_653E1Ogo_y?<;9dm*NEB*X;Hse=NAhOVXbgXs1I!HtvKQ4 zykD2BIgyv&ITZUr!IQa6t==kbUGjWt)NE`MgAuZmq41jYflOrMyBWX~;R@Ni&~{q_K(8sRxihl#bsG@}|XFrO@!5hzYB zoz5vl;$r|lGj=FhNED6?r6UktU}ismNVsQAs$FMqqA5yZv*8fF8k5_eNccvXpmYO# zF|y+!e)dCvhebt=mkLR!c&K}&fSJrLsG}03i54G=vIz+pGn_h*)sjwqx&d;UU>;v% z5v|^~-`!liS1ad2eR{D4>nNGA7uT!$hlc91Dx`+b!yoh>ttPrt-nxu$^Z})ylx4^W zf6y8jQ>09Ye@z-toP&_iHHHjITUr)?k~Jj)2R+Eh#(4{a2v6ai!#EdCa;<$tQ2>Lx zK2k^3Iqa_e(h(z>%0XK-CPN+HHX~%PSo^(}ixK)L$jML@#=c5eRJJ{(Cm_g(8WcNj;N7@v34W*+s6EPGbb0%j{c$DIXs<_eN zgAJwlSf9>sJ~AyT+xRR^$!=@R)1I#il2?F7FA}F z>l+Z3AUbArfreX1JZ=`eYQO0TG&fL_5i$u|6$n#{R$@Z@;<8@6er*Ca`Q{^StyJ)~ z%RmuFbHl1jsM@520ZC_JuOmz-@{v^6ZeM)2%>30?D_ZHbuMhl}I4L}vGC$coP@>nIQROyzu~a9{p^Io~rlH{}z_Lml_q>5ch|-6r;~!S;F+Oo1Zp z!BU3#L`$;P8Faj#+Ju~B2LhJ0^Y86)uwEM1pQoc>hGh^^Ma^d*CjQpeD@Te%xyF=s zf@|FdvzS&*sEmH1&!HX0_eqL95Sbfe1U~05B{S4%ltwCy9SWcXNc8p z0M$KEfGP#3IcbANwOfAD+k|^;MdAjAV-R=-{C9$ZK3twFLGIQ*01Uy%&J=T2(w2E! zV~fYcZ45*>sku>Fk~Z_2i%u1ev|Ke>=LCasDsh1jVPZ)NAXq6ZF%zojk=Hn|Bce3K z@kr!^A~%hamgr-kcBt;;jXSB63jHz@C51JQ*rvXkmROn}4{w!IB$u#9XRz;~O+LFFPD2k^|1uNc>t8Wo5P%BT-U$vNRrHbLa?&KB-|$TBF1G1!@+cx8CJ zpR1a+J0o|Yk-5vFaIu&hTmy!0e*h=_HFwfFcaocr{|i>jXUrBaZ-sAWC%m9JMjBAs zshZ(#)jdP|J2{;z`gC7lWI0T>SX91PS5+8T!GnyE;-UsyL(_TCbZs^kyQ~#)YU}0; z2k$TH+DM&pcgGJxD7SsRP;wi9h1lJJjUf-2QGWf5+)>0$LzHcX44s-dy5t?BsP+0Y}VDMxAF`ery|$ewN56ll`B3+jaW8^C^WE764i;`O;C+zqY8Lj@U^+N6S zvG1j*`j+{^-c9d_!bm!cpGH9h|51(UM=}OnFG1M)If}sZ0D}RdJh#21UOP9I^r|^# zyUe0Sd01yf*k*ayuQ0tQR+ly7?ipU|J6?ca+4`Ss5Y$t)J`oXe4z_mBi5^p{np;*N z0KgCUCu#BTG(%elLS#Nr6$M<8^$kXBP9Tf97-NZT zd##1?g-z1nPF=o;tB4%1NUcJ$bcD*V>#aXBK|@T7Io$Lv0Dn~N z1r1u6pYxoC9lwukcE1aiRm2A(wa_XI&52K@G4u>t^)ly(Q8s?G7`*Nv4TMI%ETD9* zs|^_WZ=6hh_Ay|kGib2}!y!BM!zYH)Su0W#l$5CS(tr>SshxP->n#Ex6BJIfEM z%W)^>dHZ$4>ZA0hm6AObfe}k`GrNEg$_%OFu*>;XuJ@~T^}0ROdZa7R26np9oHH&P z{L=T^k%u7+xKs+VqHLnYX(Xd^(&NU@wqA3HmJ;@ozA)C1O7xdgm~0uzpW1WQJ?gWM z*;fqlQXfZVWp2N>pC|a`i=WJpB7-1%;*){(p0-WoUpuGGYik2z2Xh>e%kol4m?%s5CbD?3aorAikeM*b)E`VFd%J$s=rKti}O~No9 zab)csYI3EC=y#+;O;I?I1CLP!2dRkX2jvPId#yFZ3V=7U)xACl334VfMWN2!TS(vC2Brp;!rE?ZJAxZ^ydJZ%JN=%H&l^IxLO4}X{?=n6P=fvyA)I+8 zB#oV`^xk$Dd}C3ZX-N{B4iO@ef;i_(=)-9DKx!p|E1yz$WJZF(Vlf*M{qh$n;#2B5 z&OexJjC`P5U6d)UJW?5=on78r7}2}b(sxP)v(WleA_`nC2t+M7&EOynm zP*?-B>u%0p22$l0Rt_0XE2udP_9lL;VBJ#B_th9{Kh7+jjH5)AITHDGIm>;^<79ce zM5=PADYf&BPNUDs$l@ml;4#4hQXN8`iWDcWT~6Hwhd{L1^6ORm9xomxcSK>l>7c0W zhl6^-nV`-}zuSiptu#C4{Y;)$F{y^VZ{&E{V^UEro0X1Eglxt-juq;53bG$sBxfRJ zgzo7VD=gM#!>-477~NCGw+fDBG22fGDSh( zO23D%Gw;L^H>+>a*A(q6tF#z<@NyNCEslivk9V?2+kL&uCFUj(_3rEp;|leTw%YjKN!R-O$;Mn#3~A5yw*~(4iWAq zj6FsQ!=rdNnJZPb;uPW+^L7)zrs`!rd0O2O?U^ii8ski#`D#R}?_!&2lYe^CnC4rt zHv|+z$>g3ds21#Gv)5>=1iz}=P@$i+j2=3qwv+er8Kfcku`)LZfO=aam_k# zkoEa*&uJmx`Dc@`l^Z$WGc`5l$1G!ruVtX&G3iRUoG~OlOZ?2p$h?%N-IW~@06=*% zho+#djnfl%uB+r~XY8o`J@e~ShGn|x5$_TxRHqy4_#o@HIyNA2S3$Jg;;;=WjBTOs zxDPIfz)}=48;!Y{I8ly8hBo(?XBUtKn0NDU%o)a*?4axHXPCV_n{c!@Rci*Pf`Z;L z%=DVK5uPOp2-N|o4Sn!&NnU_chYZU>y__j2IZ{FF+jxL2n)ET}w}`?_wMeDKq4B=O8Qv;<1dP^Dwl9^=f*cz2{G8aNa6^eWugF{rX{BfkNOOY83DoJj+TELHCRa#0US|WMMG()!y2=Y zdE6l056U9%W{uF2)oFGeHI4&*1VR-Yle9G3i(WO^Ff++LmNCJ?#exUpDwD>LvAPZd80)r&wxWKNJ}l6QgQx!^R! zq|Hs#!&$nZ2-LO%*U0{g{-1len(GpA?2`*OpPuio{C!Zqc673}{_e{3F@ESSdc=UW zlK6!sB@E&`bGG$G;nxI{<~U|10%WvTS0bPYt7oU({gzEhx}k76>|}8L)xN-kXRBtD zC%d*mFsQMxa3=Z!aGcOi){arQ^khK_M&3f*X$pl2$ecNB3foeR;kd2hu6^RC?61O9 zKa_|{>?~Nmg8O?3>v!pkaZjp$Pf)}^m!NNF z_qYE3ht|a~F_X18_D9)G2!pk7%uGZml5TD!z7Tdd*J~Sf_vq+V0tT{$TUEt*fZ9zi z84aGmr8QzOQ@=49|I96Tns4Q;6rVwjh)K=^`bA&7LXP)7&G>iPGwKq&%fuW ziLI@ZjjhwqCS)dlRAz%7v0wMaS?g;BAPr$@4sX6sh|3;;4fcqDqbe)%EZg?D9_c2U zVrXvrkaiQ#^Ni&AgC*y(SQYk{ov5Um`J_mJf|R4>gkGM!G@da%5GE4&9l^eb0p#_m zuai^(oqUKq{Jy8Irmp<^h*mO1J9QHyFY7kpxs>z=`4tPP2EmRx5}XoD_ULQi8&2@M zml>uQWr<}TWkIN6@l{rtVH%t`i<8|f4}td*!EUG$VBL%g*dSn0bM48Zf_q5|od-qv z?b%%#9M>c1zINcyN?{T!kfT}vHZ09|nA;?~yKe#JvM)Yb2cByym4 zAbpB8=0{)9GYcB8Ie@Um>W4JzC?-64Q_xx!>oHbRVtzV#L8Ol2Ju#Xh^JRm4tX{w3 zc9&KW%r)3TN9YA^TQc!0kqrx;$Zse%vD|nQ{7mVsJkm*iP?OBv)HV0--rk}xJ~I0$ zDzj@`kLvy%zLU;(_0~^e>-O~EJv~3(Z~i=B{)&Mgr;T(tKRhrItZ;=ulXECtI79b{ z4ZPk308WJvK0wZZtBpqcq2=pCi|4rw&nIRyd%H9mOmyA%;=X?U9-LZw+S@qC(?MAb z$}A*EuMyoURD~{MnD&&Yh6yVs87~<$J0u>YeNK&zu$CSnR?k5G){|EvGNSV-N@Jg* z^xyY@#x_R(2I*-0lFbGoVxVsCI!|Y0)67ulR2aTWYe~v7L?&i`VFzKy)%KCIJLK30 z<#<-MR)&IP+lOR^QricMgxW4wsC>c&#up*7T}ZvYOSVlQJiS%*bs0 z{~VcX^&_<-aZm-Hr#hhhg3#DUf$5ebEw*)Vn)#ZH`P;-^bmK4VT>^oCnWqhs_Ygzx zYoEzaFynH4;;D#MaGQ)A#VMEr>O%Bd!8lm@(#Va4hZC z2Y*k}j+nZ3mWBg0aPdW%oWP826dWGjV|KdoftzWAeNTn+~L-dSb6*=m4s zB$yp&y^(sfFzfun+WF>fr)D1m0K1Y3vgMPxZK+Yl**+EB`&1rcOJKw%^{HJR&-@$K zLuG#&s07#hlY3QNbQy8-`(rK7Qv(^CubgTTp)Uw-OA zU;}h}Cp?Io)xtUpHJzwuFAe5}>ml@BET9w1`;smVFGWReH1P7pFssJ$pK^-k_8u&G z%^I~r%X5~hRgJ0V%dr<#70rL<vVT{G@~e!KivA+y zSthKj9j4|{lBIj1Navft3j*!z`anDlId_fx})8a1&hsB zXN~867doStZP3g#6|eS2b_$Rxi=Gtz0WvXH7cO2`rkG(m07ZgF;!HU1<@wq6v#<*b z!k+39EtNXbp_<$v-A<=zhaq;@r`pGxowHWd= zg}9|%J~|gSPc52?fZX0;7s#Z_wGzm7S~+3uJ9(c&2|KHJWzPDuuL5;T_EJO)g_o=} z)pzg)f#Esi6F4_kMj&3D*05+$a_>8dqlzxAiv6{VA)6$9-HXK*bHyLkwUWuus+=;& z$_&j@$%HH7I*@Xi6CT!Ouzb=U&9CGL&s`@Z5iB>aA#58s-I%CB{2K10#5N1w@|O-; zt>Hrpo7Ir<%d@a!s%!5g-MeG7HjU`=cKIh)KMW7lbOqUPdX?c~FJ9Bd*M?gz%_SG# zgwYW8XQ11oN;g!@-1HnxQVBM-Rae+9Qb?6u;_>Jf<91M?~3PoXLJ#1{Wir`b3u=^I%6V3pE8F57ewqC%Xg z5`ChsOO7xvU7?YD)6NY5t`mYUSX-U0Cn)=1xgN2MhIsb4C`ci;^nQ<+|fmKTg*D&;hUKcg=Y@PBp-V9cFl!p%JMr`nV?4`*}ZJm#F zm`*rySlQDI4b2dfw=wFi-28gVd|j{T6q%wz-%|Ces(XQag(%$tx4TO$OxV*qBh>(5rqlHjas~i#}W%#Jou9ny&V-Z{E$9QoDi7v7mxAWUeWP zHrFq+60PSt3svOzxp1VUO0Vcq#>6a~4mDBemUG1R!ZoCCfqEc*EAwMaiIF&M`7b@H zHtM4Y`o$2I(@b7`eo>i1Aki)wPuS6^vFHr)YjS_inIISL%Q++fAVC@gfc$ID{+z9S zwOuRhYBU$LAQziaEae6fMbv6Z;pmljW27T*%uXV}wE1X>_OzdAijFm3BYkS)7DaS} zh+z>$&GRz&#EEBFqVI0ao%qZGaC=3U0xL@T9y_hKTDURd8}c%VTpT&jJoNtl=&m4; zSeu%Lky4>&5pSS|?YghpHMyw=wcwe$itHB_mQ`6p_DBoYSq|c{1!IPykNU#kelZ&Z z724*ZR>lP^Ng-Nnbh&I4;A6eHVO(1ITe%}5uNLAclWec&)u-?kROrp%BG&?0o^AC1+(4}EiFEJ<%HV~{gu+{i zJ>mWAhLkZY(|fFn7ld?*1A9ub^@)ln=-u3=!iB9#8TOM}Y!lJ0Muw-!5^J`b34@*N zY4-?TsUCL=?0IAQ5$p#wvPH!tH94Fv*4pR7K3G7lSZfNW!TknY)Y}S zm|5M4Y8Txj4dyUuB+K>XF6-oSgZjo4vCapv7Rwwvs_aIXCJ~HxSGW9V`cSqLhqnDX zkM|T~AwWsXmG7x3?{X73zNr}*N=h_|969d5%^JUCp>h)*Ji^L1*>ItH*i@{?my?0? zARJ?{(P6uUxns1*TvA5JhUT8(@H`1|I#z;=A(L<7($*ZVwNv7-W?z=FzEzK!WDWO$ z8QzLfk>2)WD7R?KD~%Bv_I`V%Yq&9V6N=EjNVV!37cK^rJzYpqPfYO!_HR0z+488@ zGW1QcpByU#6*R1KHM44$KemN7-KD+u2;+EmM#?iB9#VVp+`JXcN^b`Fi@SL4WkliL z8DdWziQZZB&GlLRiGp5|ndaoSU4Nb8ATr_9S3k=upN5R+^)-EUnY4lS#A8(7VtG47 zwvR1qm$d`pMc?+skKffGCbB;?*#q_(p{G8Qt&u@1lX^(m`;3{ZA5>-E!87lhD~*M+ zX+T2sfNER$b-!fMdDrJeB=*4;x~sx9=r-TSC#ZrF+JG%9lkl*?RdRY(n22oc18pj+ z*kWjAd>saf^H-#2DLR#tA@T0v!~JWCb4mh64nl?5dkaTn{Xy%UFtslQG@43Jc;P9G z3Z?4`&ktC)3Uga)x|bwu=1Q+r_%`nB z;xryh*CTw+HiQ^eIn1AYG&5bJ5N zIFx+hL#M|5u+cCl8)QDq8{pjxuT>=)LZjSQg}NH`fU!F^$Nw37(Sbh_d(rWIEVlY~ z;dtK~)>k}tg!p)H>aE$|2gix3=nyT=p&-%+7wig@a(I>?h$iD)dzg z9S1wsO&1T7{W|uZaeHQ2A>XJg&q#gq#y~F&FNH3NospZGshnHy^i|{s>6|dzlWIwY zKHV~4|Td$KW<5NqU6*S0(7#JQ&krUvh4rhEe8!c9wX^LUP zK&f(Sjv`b}PSdyv7Q^(ic78n1e+PYH9|YuMZMsSxJ=Rgpsh#_1D3P=a|d(p%i8pMDaVPaKU?s-uFP3Wu5CpXidnz&Q~n2vkUm_%FF!p$-@3>!|7u)untn zhoGe|qjaN*!DQnioEn#{-h-<>d274h<#x}9HQYSy))GjAHEbL`h;?c95Ig}yQ+%$b zweGwMOVt<{95ZyT=CFPnXxnG89w1Qs9t65LP}nN2+S7cF5~&G#aS^!*KW*^F<6^H5 zo(rkL7yKZ!x5*}YK4w=)^I*`N5vSIEXb@u$TXSg8+@q%s9jo?^YW5Sjo=VFb)sRDR z%;c`3Q%2svkZy7KxP|m);lPj7y4O!O>bUz&+*1pe} zU8%+K)s@*5$!?Xji-8rSii?*8B~nOqbFYGbHvV!PCg;d0@v8`M(s*0AEfgPprrm~A z?E@~W;X-20kwW5E%>d2&aO;6PBAbSObUUDRfin{o`AR>#mqjlULLV6drT3MS-|$Q1 zfJuD!xRvieit@{FM5+WlidU}huz!sfKZVK-!NhtXQp-Mo%W9^-dG)h7+i zt;SjTz147*!5wmxVcKzsHBsl|5@r!OoED$@Nw_0B=pFQ#qvG3Sc*@pqyGEa70zEIG z$2v5xpXa`@4$XBwD!vcD%z7(AVrcljga=y0N)O}WjvoUcaO))_9#@q2<|Z`$wOUa6 zov?hiZ!g3Req?T<;7xNUH=e;6a=mZ6S(XAJT*sZ3oJz%w-zd0DfE~U}93EwfybScS z*G)4c39PYIH-qWxKnEVJ)7MAbv!BMl^k}2*bEh+K$zMCTrhRA*zV>A3OvMLDWO7s8 zLr8un2MfC=h07cqhY3 z0y*VTmsnBIlToP4jsl72mE>vBUM3VwH+~CVBgRo#R!A|aExMP;ss13S+7!hQH~CXs z-yV>Qc-k0g92O?L15fh65eYCDh|vqVtM}8n4Ib$Cq%fOX?|u0wneKDd#`wuzxO(Zg z9XfYkj!lzjewo83Ol0yhR4{~~(+tONW8`zt4#)pKARp0BTB^yZylm6=i9;DKOxKvF zWI>C5Gp!arr1(+BDoY)`3!YBj* zdVJ!8POZ8Q|49cUPn~#ET{&aV2L*16_*URYZ0^J3_qaaIN_yVFoI6`QUt_7`%RR3j zIffd1ksG#vOYwTwQ8=*XkVR{cX2Pb>Fv_ea0&~=q5O$EqSCk)Ce7+XIwYE>O_jEBzM?T??u1{ z9BPQW2JD+D4aK_J?yne`Ys##G+xsMqtXUqf$k7KpwiGv^v5r$*?-DTWm$DdR%=!mR zbWWExddb5~Us}oOX%jK9U{f7`qBNmR9r>IVgRY4m3uhM#=k6-DSl_8Ch7^(Aj~mFa ziss9c6_rq?D}ylCy_}Jt$0-$R&bn8^Y`vbl;gy%3KR+J^X|CH?8QB}|5v|3h_6>52 z#jZEwO;uR8TDO{xG$oR|IbbOQj|prae(rwG0bOlpT+b zws>PgW{lwdSXu_MLGg{O1YgC6MWN(K_%5d*zYBGs!vV%&q>!wHeSzyk>e+#Y31F`? z`s^T#ie-2QNdS!Q%h`d^y|WYi>SXpvE}`Dp{)@u-M#F0H0NNf)zxWrvYnbwO?`hU9poAN0<&FQN4U>Fs9U8Vf^8tbIU1TJ86j}BUpruq(cEik-9Ryi5O@wn zpPrkQMWR&ct_gJXG;^`^H0S}YQMI$hH7DhH1XztdXY*yTduZ=%k}?AWKJW(aNUcH2 z4{~XL(l0cInyk4iQl2FR-pmE3+xPOQnqibup;Wk#zUN)bUc3q^2cpjkmYU?O%rt(; z2;~gLrejyp9kENLv8UyTT7wuCLnir>0p4`N)#6?aK6k;euRIt)pf5UU7aKX6kO$U*#ZF)p-STHDf=L8c|jf z@tl2ZS--6>K4Q6WggA+`05DTe>_qoiUViX1Hd2#?FSI!#13!<&z1HS>hk{a`xk)Ry zRVz68ZBMQu?FuJ%!Sb1??_d%p|C&`wp5b<+w|(yX9;?n(2=zl=6%z9GNTSbt@Qr4- zQZPbDZOWi;yZUqzafDqBN<&$po}#!{k~{23ZwC_x)L~`X-9V$8596GLv-2kG)~5IQ z(5?6bPKbtv*M}?!CUSCE15ycZc;?(L9%+eNI7NqZYR;X_gsL1Z7bB-{8|78{Yz~%^ z&>Hr1U$;dl>Mhudi+J|ChVI^B;HRZsmy4GkR9bjMr>|9m<|7PFIY#OgssPal6RyA< zI;nijAD;cqgs_>KFS>&P0Mlpy0OBtu#M;KuMA!N|Yqb8(RBq)J(mhp@%T$vzB}2L= zc?RQP((3lt&Cl7NOC>&vQd%|NhlitY1*Xb3)ME39*tJJ|I-g-y9->uW?RMY``XC0G zZEcbON%R>qRvc<$Z1pl%|HXU2b2q=|Os_Ef5->LPPq;Axp)HAh?&c`c;KYfxlQ@#; z?d!4$Wnd0~7Mg=zBG=KK%W$1;_+#M6+i(Dg>z8G#lm(V#Brih(}{m^dsZumI4B z$13-BP;;lpuZuqiBrOYWi=3~^|3pt`1`U5Zo zdc0TxA9qfd1z%@VcL0V!NNc>|>z57F=DJ1?(Hj^@@ZwVgG{jAY<)^KRY>AogXzZi3 zhbTq){RN5Ml7qmBV~OZde(tx$hU*umCW=HRBM`c(eQS(92yTJuOjf&@Fe(d_94^Q$ ze|Vwqp=yk!I#Ti&J^0ZaAo|O}H>J z|A}|dc2fsIr=5`-LT4{k7V76v)Q4=u^9bZqp-_oP7ZWB5xW+l)6io~R*@fwwTKU|% zqs6=ROEHBUR1&-7D8PK-$h1=yx@UNLDa}b~Bkfg7^WwYrcfSzFo!^~&-cphQO}%y^ z#t%=EmsRvmCk_?b$)`X^%zFXCOPMPfa{qDNbbXdtDSBRr3vIkld4L7e$hWk2aBzJ* za%(bRpK;7u!lSw#o`&T%a3H&O>Rd(gX1=Hj@;b#&IIn3GDUm@HEBYGvT;<>2aN;4YqZ9-5q$4>_l}u57vj zensp5g+C0R3kW#BbBX{x{E4hs0D>+;jAG9 zK?@R@z_Jvv5M^g9YLl(SOoP(G#A$=ckRwBU^RC4X-YS&0lCLvggaaj^XVPrxax>&R ziV9wmsJYP;F%!vH9cfiNeI9a3xP`n|SG+|E7Jn^wTk7nHEi_QM&q4xnBqmDA1Z#nq6`3!G77|Cx?uyYzgQ4 zIKc!60KoHKE2o*QskW|_p_%^oS)Nep3mJQbdKcAwlNZ7Ci9Eg^8WWy!g+zwCMdIsZ zzD8M+PThwg4_p)El&%=f9Eod8%-KjW-Sydv1?!NKPbCSvb5KH!pi~mzQoO@Q?!Jzc z3~l-_m#j_AFF>WRI~d&*A2kRZg#mro+t*J$?Cf=$>A zLC{-R@AdJ|Q#CC%4UyBJXnrc*ef~r<*!mfRkf|qG5S0iW2()!Xud3ioX)vZ`*i!;Z zd|(tUtJoIG^(@2X0wg?HyAXr7H>>Fet_v#EvmEN|Vh*CPp@#|wXvj2?*h!9Xj+7@d z>pv}CwN)|}emU=i+C+Z1dUwQ!S}Bd6EO<9{0L(}9ba{3*|E!jE*!~wG(WWR6_HFrW5Acqc`p1Wh5$#3h^$8lk-1dJi zA&`%5`=5@K@H4`rHy(c*e01;cXHP%-{5(Ml9})wjNAlGm>2KtiQ+$(YNGH1`k;%;~ zqG#_pfKMt{WNuC#ptv(D5E^&8(Z?cyULY`?r?j#nH~z|0MHky)e z8?-12jsW=sr4C)&wu@<*BjpmYj~dt{{{GtAr!j0nlZ@n`<1}QnL7|Iqn88qN&S=@! zp*>%ygF%DyOQ|U=)L)0qq#o3W0>PPOoJ-_rWOr!eZtoJJCx=YbFLb zdwDNLt<1x=wBIT>e2F^8j#pc%It(LmMsQ)h-ujY)i*zKn z=vyrW6`VRuno8CNiR*w}6P7qNTn%sRfWlwV3|cPrw<+H_&Jl$_nsDv$`EJVJJEN_& zjk)P}Qzi}a!M43Z@?I{8oSBoyAj&diS(z2I#UC}qHqhfIr8z$r0!LgrK57RV*TrZA zz-O?M!UM}ZKzojt3`P&P%zdAshQq<@Y4O8zz}lEvhvK{<^_A6ed()mMn;Z3*1E~=uI`F1nA#+JP&|Sg|t~hID+aenJ&E27!SL?P zWB`f&Jz;~7;Z*dv{QSFd>Y1C{n3>!BtU4wl2P9WtAptc=jvH-dK~w}K*xY!(BF%pS ztg;4s@0Xg8rI|NJbV$@u=fiSY`Zw#joTS7}@6Op5g)6eswW7XM&3=jOBS0}wOyKIm zL+ujQ0i`FF-Qwr5=S^0fU>lk5;;FQU%pCzk9jrB?h?L`YVjW{NeEE%d1VAa@gld0)#d;2xOP>N*4 zNH7~+M=9oPX`vLs7t0~!*2-qoNZutrHcQ_oQ&=2tQ{<9mgU?@YwBNm1AwFY8Z%PBK zkQ;RGTiFIDyTP7#M9fHX|6Ioo-8sg)(jH*mRbPjRvEC7+Q8Ldy9P4?$G;$SnYd+bX zxkN}xVrF0aLb^9bG#h#g^4D-(KJ*z)1K*+YT_|%bAhbbCKEa_~PGf2Kr@?{(!=uqt zLS+=U;emL`Z>wa3)mkOjTQqVIj($e!Z%A;O;)vLVR*kqrZcxm^xp0U17?T>gC1N~b zMw!~F%I{)7UZXHPFnP);u&P`R{Ja1BTuevpZ>voo-PY;x!F_ywTz&Yt!TjX|KXx05 zAU*_8LO8(^{yO^r+93M&elrBkQvkdo0fM)*HfIyH+I_>teS^!18TVHvG)s#_YD{#E zWKj=K;3tmvnra)^2jjkJ^a{+x$hJsMB}#A3!Wg&ZDfcSGZe4>Lz*v#scva8w=v+ zp;9q{xh(`O=NtR>&d`HZ3X!i_8tHT5%i&xU2$-D3u@+cQUo_SMP}=zrqYZWJdfqx+z9tcp zcD$MGGY~Kc!^9r44kFkcwPTrKCLGSMY?i_?981CIq;SZ2D6q_KN0k-UFCOawtxs+h zIpGGCM5Pcj6S`bA)`jg_kjcc&I7t;RqEblpwy|)xkw;c>U><;+@BG(vQ*AKbYS`_2rlZ_)~QTB zj`-(nEWFtLxr(ualr!IJDS6H0n*pA`m(@T0^2s%tKdt`1?UoWLy}fCVZfX5^ZRlUO zwAQt;d8|2pFg96{^Ja6e;Jh_Dhd&Y3zYA>5Rkfj-Jky>QtcKDgnL#I#@pw7cHy0YR zTEoK|#-tR^cf=u_*||ICI;qnLE5lLvu5?g2TbebmG;ey5heuIE8671Rd{tV(q=2Zt zu@K%RoNK_*8?{jF%jYddrmy_dHZFoTh@w2k8Dr7YR}(>&XuF`%P8~a%2I3TC z0ynyY(7-K8iVH~_MXgzi<5W0D3ryAh7-Q#Xp;kMeQ+&1bY9Cw`X=k<)s0$EC74#+g z^YuubfwkKY+F|%3VaS%$%-4&5!#EI#2pJE0?w*RAjhcZ-H`RTfL#_%iWo|R4un$5y5SA-)BKe@G)|? zis=G=-a%VPyQNu|f(Xx6ejUjb8!(}nu3gVK7WvLjY7CGdg&q@B1s$EK0UxO$nNL6N zjUvV^b}Se{cXGV(#O(ro*g85N?b-F1TmEa$59X%duWkLb=lHzU3IQhMlBy-EQusSL zmaz)5r2SKexo4?vZA`atsU!6ovLvE0Ns=DIJ1-X_Vy0T!$3&R|iJ7CRls%oJ+##ag zA=6$kJGkC7e)aIS6?r~RE^2I%jn2u%U4f>^FTK6j1~#gADG#=hSWHk6E91T=W?>Sk zz)^jC!CyINDNabAd-j^T>=r@WCn$q#82idZ2h6qA6b=nWI=Pi7H1EtL-%BkYy6&xq zbH2!Irs#w6`#4gxQkxXgq7Q}%q=F?8Ey$TnQTHp7Sni1rhUd}*Cyv8!5slX_q0DPI zoEWLVJZo;nMb>gY@)h=&EaSln8kCdr$uP5GDywbA+&N=3*7a-fw0Xsq-S>5uxA~fJ zxEA5On7yQptOzomn~KZ73Zy0grl4D*O4O80Ty^Y^QVP^Hm6e#!l8YCe;c{!_#SvZV}V4ow&_s_ZlT$gcN6+hV+Y!(=SmDEb>9QcA1Z< z`+g{jIzf+(dZ;SiH`Tg-l}X_i>iXpN+5cs!`P1_M+e^)def3G)N7od1q=)~=)68t- zwX{us&`3!m^WSMCs6!>fuT+(B!G?tk)Nk#Yxd5P*ZxC{pm&U8{i>i!Qg6GkYjvv;Y zO)iuy_JH7JTU1RddWl#i(yFe7mlsn&?RG4|?Mj<1iB5B$OPA`#Nakx1>MX9PRLX^p z>1h7XMsOl3+Q!b;h4gMg+gKr>I;=@^-uwo-6;Qh-4gQ>cu=kp6RIrnk3LCf+SV5tm z)WKK7{gPkIji#!pkI-V;U`u1AkJH!GLycaCDK~QQX)5rxvD3;ih6a43>{L>A1-pPM z+yuEoktVlJ(l^2L5$VbMaK@Q~iCaK6_z(~W$jmU)qqusdG@`2;8Jwv{8Sf}qB>W9> zK8QCeML3iX?|t~_q_PUmu9jWOd;2dp4AaA)81?@@Y0n>1`oCdx5KvUWf4`ghn4>+m z@0G^)_Rme#{|@=i7yNABA;gc*9~qwX+h4$+$>7twtN$P$U*dlQ|Gb;|V~UwUqi(iz_Z_H1XpAtjAPyj&6 zznK4Mo~IPbZ(uR@ev@rQCDueJJQGX_^PvL)R@;%S}-69@? z^^aKnQ^C;s7wm^e;mit zeeSm+S>*pn(t9NQ{@wNdZ<_pV+hdBqH2F))KdM0d2t9NFI>7lcqmQTj{^$P(wntnN literal 0 HcmV?d00001 diff --git a/planning/09_Domain_Batch_Repairs.md b/planning/09_Domain_Batch_Repairs.md new file mode 100644 index 0000000..ed1bc8b --- /dev/null +++ b/planning/09_Domain_Batch_Repairs.md @@ -0,0 +1,301 @@ +Music Store Management Platform + +Domain Design: Batch Repairs + + + +# 1. Overview + +The Batch Repairs domain handles bulk instrument repair jobs from schools and institutions. This workflow replaces the current spreadsheet-based tracking system. A batch groups multiple individual repair tickets under one job — one approval, one invoice, one delivery event — while still tracking each instrument individually. + + + +# 2. Batch Lifecycle + +Status + +Description + +scheduled + +Pickup from school scheduled — instruments not yet received + +picked_up + +Store has collected all instruments from school + +intake_complete + +All instruments assessed, individual tickets created, estimates compiled + +pending_approval + +Batch estimate sent to school contact — awaiting authorization + +approved + +School approved — all child tickets authorized to proceed + +in_progress + +Work underway — child tickets at various stages + +ready + +All child tickets resolved — batch ready for delivery + +delivered + +All instruments returned to school, invoice sent + +invoiced + +Invoice sent to school — awaiting payment + +paid + +Payment received in full + +closed + +Batch complete and reconciled + + + +# 3. Database Schema + +## 3.1 repair_batch + +Column + +Type + +Notes + +id + +uuid PK + + + +company_id + +uuid FK + + + +batch_number + +varchar + +Human-readable batch ID e.g. BATCH-2024-0042 + +account_id + +uuid FK + +School/institution account + +contact_name + +varchar + +School contact person for this batch + +contact_phone + +varchar + + + +contact_email + +varchar + +Where to send estimates and invoices + +school_po_number + +varchar + +School's purchase order number if applicable + +status + +enum + +See lifecycle above + +scheduled_pickup_date + +date + +When store plans to collect instruments + +actual_pickup_date + +date + +When store actually collected + +promised_return_date + +date + +Committed return date to school + +actual_return_date + +date + +When instruments actually returned + +instrument_count + +integer + +Expected number of instruments + +received_count + +integer + +Actual count received at pickup + +total_estimated + +numeric(10,2) + +Sum of all child ticket estimates + +total_actual + +numeric(10,2) + +Sum of all child ticket actual costs + +approval_date + +date + +When school approved the estimate + +approved_by + +varchar + +Name/signature of school approver + +notes + +text + +Internal notes + +legacy_id + +varchar + +Reference to source spreadsheet row/ID if migrated + +created_at + +timestamptz + + + +updated_at + +timestamptz + + + + + +# 4. Batch Business Rules + +- Batch status auto-advances to 'ready' when all child repair_ticket records reach 'ready' or 'cancelled' + +- Batch approval cascades to all child tickets — sets each to 'approved' status + +- Individual tickets within a batch can still be worked and tracked independently + +- Total estimated and total actual are computed from child tickets — not manually entered + +- School contact can differ from account primary contact — stored per batch + +- School PO number is stored for institutional billing requirements + +- Cancelled child tickets excluded from invoice total but noted on batch summary + +- Instrument count vs received count discrepancy flagged at pickup — requires staff acknowledgement + + + +# 5. Batch Workflow + +## 5.1 Creating a Batch + +- Staff creates batch record — links to school account, sets contact, expected count, scheduled pickup date + +- Batch number auto-generated in sequence + +- Delivery event created simultaneously for the pickup leg (see Delivery domain) + + + +## 5.2 Pickup & Intake + +- Driver collects instruments from school — delivery event records condition of each at pickup + +- Back at store, staff creates individual repair_ticket for each instrument + +- Each ticket linked to repair_batch_id + +- Technician assesses each instrument — sets estimated_cost per ticket + +- Once all tickets have estimates, batch status moves to 'pending_approval' + +- Batch estimate report generated — itemized list of all instruments and estimated costs + + + +## 5.3 Approval + +- Estimate sent to school contact via email (PDF attachment) + +- School approves — approval date and approver name recorded on batch + +- All child tickets set to 'approved' — work begins + +- School may reject specific instruments — those tickets set to 'cancelled', excluded from work + + + +## 5.4 Invoicing + +- Batch invoice generated when status reaches 'delivered' + +- Invoice shows each instrument, work performed, parts, labor — itemized + +- Total compared to estimate — any variance noted + +- Invoice sent to school contact email and available in customer portal + +- School PO number printed on invoice if provided + + + +# 6. Reporting + +- Batch repair revenue by school/account + +- Average turnaround time per batch + +- Estimate vs actual variance report + +- Instruments per batch — count and breakdown by work type + +- Outstanding batch invoices (accounts receivable) + +- Technician productivity across batch and individual repairs \ No newline at end of file diff --git a/planning/10_Domain_Delivery_Chain_of_Custody.docx b/planning/10_Domain_Delivery_Chain_of_Custody.docx new file mode 100644 index 0000000000000000000000000000000000000000..6e4f5721a3c4c3c84cdb89dfd46ebdda52565385 GIT binary patch literal 12928 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 XqYA`dTMr$84sd&}=#QTP0KoqPTID-T literal 0 HcmV?d00001 diff --git a/planning/10_Domain_Delivery_Chain_of_Custody.md b/planning/10_Domain_Delivery_Chain_of_Custody.md new file mode 100644 index 0000000..830f1eb --- /dev/null +++ b/planning/10_Domain_Delivery_Chain_of_Custody.md @@ -0,0 +1,327 @@ +Music Store Management Platform + +Domain Design: Delivery & Chain of Custody + + + +# 1. Overview + +The Delivery domain tracks the physical movement of instruments between the store and customers or institutions. It covers all four handoff types — store picking up from customer, store delivering to customer, customer dropping off at store, and customer picking up from store. Every instrument movement is recorded with condition, timestamp, responsible party, and recipient signature. + + + +This domain is primarily used by the batch repair workflow for school instrument pickups and deliveries, but applies to any scenario where the store transports instruments. + + + +# 2. Event Types + +Event Type + +Description + +store_pickup + +Store driver goes to customer/school location and collects instruments + +store_delivery + +Store driver brings repaired instruments to customer/school location + +customer_dropoff + +Customer brings instrument(s) into the store + +customer_pickup + +Customer comes to store to collect repaired instrument(s) + + + +# 3. Database Schema + +## 3.1 delivery_event + +Column + +Type + +Notes + +id + +uuid PK + + + +company_id + +uuid FK + +Tenant scoping + +location_id + +uuid FK + +Physical location originating or receiving the delivery + +event_number + +varchar + +Human-readable ID e.g. DEL-2024-0018 + +event_type + +enum + +store_pickup|store_delivery|customer_dropoff|customer_pickup + +repair_batch_id + +uuid FK + +Nullable — links to batch if applicable + +account_id + +uuid FK + +Customer/school account + +status + +enum + +scheduled|in_transit|completed|cancelled + +scheduled_date + +date + +Planned date + +scheduled_time_window + +varchar + +e.g. '8am-10am' — for school scheduling + +actual_datetime + +timestamptz + +When event actually occurred + +address + +jsonb + +Location — may differ from account address + +driver_employee_id + +uuid FK + +Store employee performing pickup/delivery + +recipient_name + +varchar + +Name of person who received/released instruments + +recipient_signature + +text + +Base64 signature capture from iOS app + +recipient_signed_at + +timestamptz + +When signature was captured + +instrument_count_expected + +integer + +How many instruments expected in this event + +instrument_count_actual + +integer + +How many actually transferred — discrepancy flagged + +notes + +text + +Driver/staff notes + +created_at + +timestamptz + + + +updated_at + +timestamptz + + + + + +## 3.2 delivery_event_item + +Records each individual instrument included in a delivery event with condition at time of transfer. + +Column + +Type + +Notes + +id + +uuid PK + + + +delivery_event_id + +uuid FK + + + +repair_ticket_id + +uuid FK + +Which repair ticket this instrument belongs to + +inventory_unit_id + +uuid FK + +Nullable — if instrument is in system + +instrument_description + +varchar + +Free text for unrecognized instruments + +serial_number + +varchar + +If known + +condition + +enum + +excellent|good|fair|poor|damaged + +condition_notes + +text + +Detailed condition description at this handoff + +photo_urls + +text[] + +S3 URLs of condition photos taken at handoff + +was_transferred + +boolean + +False if instrument was on manifest but not present + +missing_notes + +text + +Explanation if was_transferred = false + +created_at + +timestamptz + + + + + +# 4. Chain of Custody + +The full custody history of any instrument can be reconstructed by querying delivery_event_item records for a given repair_ticket_id or inventory_unit_id, ordered by created_at. This provides a complete timeline: + + + +Example custody chain for a school instrument:1. store_pickup → 2024-09-05 08:30 Driver: J. Smith Condition: good Received from: Lincoln Middle School, signed by: M. Johnson2. [repair work at store]3. store_delivery → 2024-09-12 09:15 Driver: J. Smith Condition: excellent Delivered to: Lincoln Middle School, signed by: M. Johnson + + + +# 5. Business Rules + +- Every instrument transfer must have a delivery_event_item record — no undocumented transfers + +- Condition documented at every handoff — incoming and outgoing + +- Photo capture recommended at pickup — particularly for school batch repairs + +- Signature capture required for store_pickup and store_delivery events + +- Instrument count discrepancy between expected and actual requires staff notes before completing event + +- Missing instrument (was_transferred = false) triggers alert to store manager + +- Delivery event completion automatically updates linked repair_ticket status to 'delivered' + +- Batch delivery event completion triggers batch status update to 'delivered' + +- Repair ticket cannot be invoiced until delivery_event_item shows was_transferred = true + + + +# 6. iOS App Integration + +The delivery workflow is a primary use case for the iOS mobile app. The driver uses the app at the school location to: + +- Pull up the scheduled delivery event + +- Check off each instrument as it is received or handed over + +- Record condition and take photos for each instrument + +- Capture recipient signature on screen + +- Complete the event — syncs to backend immediately or queues for sync if offline + + + +Offline support is important here — schools may have poor cellular connectivity. The app queues all event data locally and syncs when the driver returns to a connected area. + + + +# 7. Reporting + +- Delivery schedule — upcoming pickups and deliveries by date + +- Driver activity log — events completed per employee + +- Outstanding deliveries — instruments at store awaiting return to customer + +- Instrument location report — where is each instrument right now + +- Condition change report — instruments that arrived in worse condition than sent + +- Signature audit — all signed receipts by account and date \ No newline at end of file diff --git a/planning/11_Domain_Billing_Date_Management.docx b/planning/11_Domain_Billing_Date_Management.docx new file mode 100644 index 0000000000000000000000000000000000000000..c162549f01f6531c09345f40244d513cafb15f2c GIT binary patch literal 14901 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 literal 0 HcmV?d00001 diff --git a/planning/11_Domain_Billing_Date_Management.md b/planning/11_Domain_Billing_Date_Management.md new file mode 100644 index 0000000..013cdcd --- /dev/null +++ b/planning/11_Domain_Billing_Date_Management.md @@ -0,0 +1,403 @@ +Music Store Management Platform + +Domain Design: Billing Date Management + +Version 1.0 | Draft + + + +# 1. Overview + +Billing date management covers how recurring subscription charges are scheduled, how customers request date changes, and how the system handles proration when dates shift. This applies to both lesson enrollments and instrument rentals. + + + +Stripe owns the billing schedule via the billing_cycle_anchor on each subscription. The application manages the billing_anchor_day preference in its own database and calls Stripe's API to apply changes. All date changes are fully audited and reversible by creating a new change entry. + + + +# 2. Billing Anchor Day + +The billing anchor day is the day of month a subscription renews and the customer is charged. It is capped at day 28 to avoid issues with February and months shorter than 31 days. Customers requesting the 29th, 30th, or 31st are set to the 28th with a staff note explaining why. + + + +Scenario + +Behavior + +New enrollment / rental + +Default anchor = day of start date, capped at 28 + +Customer requests change + +Staff updates anchor — Stripe prorates, new cycle begins on new day + +Consolidating subscriptions + +One anchor adjusted to match other — proration applied to adjusted subscription + +Splitting consolidated billing + +Line item removed, new standalone subscription created on requested day + +AIM migration + +Anchor set to match existing AIM billing date where known + +Paused subscription + +Change applied immediately, no proration — takes effect when subscription resumes + + + +# 3. Schema + +## 3.1 rental and enrollment — additional columns + +These four columns are added to both the rental and enrollment tables: + +Column + +Type + +Notes + +billing_anchor_day + +integer + +Day of month (1–28) charge occurs + +billing_anchor_changed_at + +timestamptz + +When anchor was last changed + +billing_anchor_changed_by + +uuid FK + +Employee who made the change + +billing_anchor_change_reason + +text + +Required justification — mandatory field + + + +## 3.2 billing_anchor_change_log + +Append-only audit log of every billing date change. Records are never updated or deleted. + +Column + +Type + +Notes + +id + +uuid PK + + + +company_id + +uuid FK + + + +entity_type + +enum + +rental | enrollment + +entity_id + +uuid + +rental.id or enrollment.id + +account_id + +uuid FK + +For reporting + +stripe_subscription_id + +varchar + +Stripe subscription affected + +previous_anchor_day + +integer + +Old billing day + +new_anchor_day + +integer + +New billing day + +proration_amount + +numeric(10,2) + +Credit or charge applied — sourced from Stripe response + +proration_direction + +enum + +credit | charge | none + +subscription_was_paused + +boolean + +True if change made while subscription paused + +changed_by + +uuid FK + +Employee who made the change + +reason + +text + +Required justification + +stripe_invoice_id + +varchar + +Proration invoice from Stripe if applicable + +bulk_change_id + +uuid + +Groups entries from a single bulk change operation + +created_at + +timestamptz + + + + + +# 4. Proration Logic + +When a billing anchor day changes mid-cycle Stripe calculates the proration automatically. The application captures the result and records it in billing_anchor_change_log. Staff should preview the proration amount before confirming the change. + + + +## 4.1 Moving to an Earlier Day + +Example: Currently billed on the 20th, customer requests the 5th.Today is the 12th. Monthly rate: $50.Customer has paid through the 20th.New cycle: 12th → 5th of next month (24 days).Stripe issues a proration credit for unused days already paid.New cycle begins on the 5th of next month. + + + +## 4.2 Moving to a Later Day + +Example: Currently billed on the 5th, customer requests the 20th.Today is the 12th. Monthly rate: $50.Customer has paid through the 5th.Extended period: 5th → 20th = 15 extra days.Stripe issues a proration charge for the extended period.New cycle begins on the 20th. + + + +## 4.3 Consolidating Two Subscriptions + +Example: Rental A billed on the 5th, Rental B billed on the 20th.Staff consolidates — both will bill on the 5th.Rental B subscription anchor updated to 5th.Proration applied to Rental B for the period shift.Rental B added as line item to Rental A's subscription.Original Rental B standalone subscription cancelled.Both now charge together on the 5th. + + + +## 4.4 Splitting a Consolidated Subscription + +Example: Rental A and Rental B both on the 5th (consolidated).Customer wants Rental B moved to the 20th.Rental B line item removed from consolidated subscription.New standalone subscription created for Rental B, anchor = 20th.Proration applied for Rental B's partial period.Rental A continues unchanged on the 5th. + + + +# 5. Edge Cases + +## 5.1 Pending Invoice Within 48 Hours of Billing + +If a billing date change is requested within 48 hours of the current anchor day Stripe may already be generating the upcoming invoice. The system must handle this explicitly. + +- System checks for pending Stripe invoices before allowing anchor change + +- If pending invoice exists within 48-hour window, staff is shown a warning + +- Staff can choose to: wait until after invoice clears, or proceed and void/recreate the pending invoice + +- Voiding a pending invoice requires manager approval — logged in audit trail + +- Recommended default: block change until after billing date passes, then apply + + + +## 5.2 Failed Payment Then Date Change + +If a payment has failed and the customer calls to request a billing date change, the outstanding balance must be resolved first. + +- System checks for outstanding failed invoices on account before allowing anchor change + +- If failed invoice exists, staff is blocked from changing anchor until resolved + +- Resolution options: collect payment on failed invoice, write off with manager approval, or payment plan + +- Once resolved, anchor change proceeds normally + +- Prevents customers from using date changes to evade failed payment follow-up + + + +## 5.3 Paused Subscriptions + +If a lesson or rental subscription is paused when a date change is requested, the change is applied immediately but no proration is generated since no active billing is occurring. + +- Anchor day updated in database immediately + +- Stripe subscription anchor updated via API + +- proration_direction = 'none' recorded in change log + +- subscription_was_paused = true flagged in log for clarity + +- When subscription resumes it bills on the new anchor day + + + +## 5.4 Rent-to-Own Equity on Date Change + +Equity in a rent-to-own rental is calculated per payment received, not per calendar month. A billing date change shifts when the next payment posts but does not affect total equity earned or the buyout calculation. + +- Equity per payment = monthly_rate × rto_equity_percent — unchanged by date shift + +- rto_equity_accumulated updated after each successful Stripe payment webhook + +- If customer is within one payment of buyout eligibility, staff should be notified at time of date change + +- Buyout amount remains: rto_purchase_price minus rto_equity_accumulated + + + +## 5.5 Bulk Date Change — Multiple Subscriptions + +A parent account with multiple children may have several lesson and rental subscriptions all billing on different dates. The system supports changing all subscriptions on an account to a single anchor day in one operation. + + + +### Bulk Change Preview Screen + +Before confirming a bulk change, staff is shown a summary screen displaying: + +- Each subscription being changed — entity type, description, current anchor, new anchor + +- Proration amount per subscription — credit or charge + +- Net total proration across all subscriptions — credit or charge to customer + +- Warning if any subscription has a pending invoice or failed payment + +- Confirmation required before any Stripe API calls are made + + + +### Bulk Change Execution + +- All changes grouped under a shared bulk_change_id in billing_anchor_change_log + +- Stripe API calls made sequentially — not in parallel to avoid rate limit issues + +- If any Stripe call fails, completed changes are rolled back via Stripe API + +- Staff shown which subscriptions succeeded and which failed if partial failure occurs + +- Full rollback preferred — partial bulk changes create confusion for customer billing + + + +### Bulk Change Schema + +The bulk_change_id UUID is generated at the start of the operation and written to every billing_anchor_change_log entry created during that operation. This allows the full bulk change to be queried and audited as a single unit. + + + +# 6. Business Rules + +- Billing anchor day must be between 1 and 28 inclusive — enforced at API and UI level + +- Requests for 29th, 30th, 31st are automatically capped to 28th with staff notification + +- Every anchor change requires a reason — reason field is mandatory + +- Change log record written before Stripe API call — if Stripe fails, log is rolled back + +- Failed payment on account blocks anchor change until resolved + +- Pending invoice within 48 hours triggers warning — staff must acknowledge before proceeding + +- Bulk changes require preview confirmation before execution + +- Bulk changes rolled back fully if any subscription fails + +- Paused subscription date changes produce no proration + +- Rent-to-own equity is unaffected by date changes + +- All change log records are immutable — no updates or deletes permitted + + + +# 7. API Operations + +Endpoint + +Description + +GET /billing/anchor/preview + +Preview proration for proposed anchor change — no changes made + +POST /billing/anchor/change + +Execute single anchor change for one rental or enrollment + +GET /billing/anchor/bulk-preview + +Preview proration summary for bulk change across all account subscriptions + +POST /billing/anchor/bulk-change + +Execute bulk anchor change for all selected subscriptions on account + +GET /billing/anchor/history/:entityId + +Return full anchor change history for a rental or enrollment + + + +# 8. Reporting + +- Billing date changes by employee — frequency and reason breakdown + +- Proration credits issued by month — financial impact tracking + +- Proration charges issued by month + +- Accounts with frequent date changes — potential indicator of billing issues + +- Bulk changes log — full history of multi-subscription operations + +- Failed payment + date change attempts — flagged for manager review \ No newline at end of file diff --git a/planning/12_Domain_Accounting_Journal_Entries.docx b/planning/12_Domain_Accounting_Journal_Entries.docx new file mode 100644 index 0000000000000000000000000000000000000000..9cc5f0b457beec548429e1cbc7a01c1821e0cefb GIT binary patch literal 21104 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 V# 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 literal 0 HcmV?d00001 diff --git a/planning/12_Domain_Accounting_Journal_Entries.md b/planning/12_Domain_Accounting_Journal_Entries.md new file mode 100644 index 0000000..37d0652 --- /dev/null +++ b/planning/12_Domain_Accounting_Journal_Entries.md @@ -0,0 +1,1219 @@ +Music Store Management Platform + +Domain Design: Accounting & Journal Entries + +Version 1.0 | Draft + + + +# 1. Overview + +The Accounting domain ensures every financial event in the platform produces correct double-entry journal entries. The platform does not replace QuickBooks — it generates journal entries that the store's accountant imports into QuickBooks on a regular schedule (daily, weekly, or monthly). + + + +This is the Hybrid approach: the platform owns all operational financial data and generates the accounting records. QuickBooks owns the general ledger, financial statements, and tax reporting. The accountant works in a familiar tool. No real-time API dependency on QuickBooks is required. + + + +Responsibility + +Owner + +Operational records + +Platform — transactions, rentals, lessons, repairs, discounts, drawer + +Journal entry generation + +Platform — automatic on every financial event + +Journal entry export + +Platform — CSV export by date range, importable to QuickBooks + +General ledger + +QuickBooks — accountant imports exported entries + +Financial statements + +QuickBooks — P&L, balance sheet, tax reporting + +Chart of accounts + +Defined in platform, configurable per store to match QB setup + + + +# 2. Chart of Accounts + +The chart of accounts is defined in the platform and is store-configurable. Account codes and names should match the store's existing QuickBooks chart of accounts exactly to ensure clean imports. Default codes are provided below as a starting point. + + + +## 2.1 Assets + +Code + +Account Name + +Notes + +1000 + +Cash - Store Drawer + +Physical cash in main register + +1010 + +Cash - Convention Drawer + +iOS mobile POS cash + +1100 + +Accounts Receivable + +Amounts owed by customers + +1200 + +Stripe Clearing + +Card payments received but not yet paid out by Stripe + +1300 + +Inventory - Sale Stock + +Instruments and accessories for sale + +1310 + +Inventory - Rental Fleet + +Instruments held for rental + +1320 + +Inventory - Parts & Supplies + +Repair parts and consumables + + + +## 2.2 Liabilities + +Code + +Account Name + +Notes + +2000 + +Sales Tax Payable + +Tax collected from customers, owed to state + +2100 + +Deferred Revenue - Rentals + +Rental payments received for future periods + +2110 + +Deferred Revenue - Lessons + +Lesson payments received for future periods + +2120 + +Deferred Revenue - RTO Equity + +Rent-to-own equity portion — not earned until buyout + +2200 + +Rental Deposits Held + +Security deposits — liability until returned or applied + +2300 + +Customer Credits + +Store credits, makeup lesson credits + +2400 + +Unearned Repair Revenue + +Deposits on repairs not yet completed + + + +## 2.3 Revenue + +Code + +Account Name + +Notes + +4000 + +Sales Revenue - Instruments + + + +4010 + +Sales Revenue - Accessories + + + +4020 + +Sales Revenue - Supplies + +Strings, reeds, books, etc. + +4100 + +Rental Revenue + +Monthly rental income earned + +4200 + +Lesson Revenue + +Monthly lesson income earned + +4300 + +Repair Revenue - Labor + + + +4310 + +Repair Revenue - Parts + +Parts billed to customer + +4400 + +RTO Buyout Revenue + +Recognized on rent-to-own completion + +4500 + +Other Income + +Kept deposits, late fees, misc + + + +## 2.4 Contra Revenue + +Code + +Account Name + +Notes + +4900 + +Sales Discounts + +Discounts applied at POS — keeps gross revenue clean + +4910 + +Sales Returns & Refunds + +Returned merchandise and refunded amounts + +4920 + +Proration Credits Issued + +Billing date change credits + + + +## 2.5 Cost of Goods Sold + +Code + +Account Name + +Notes + +5000 + +COGS - Instruments + +Cost of instruments sold + +5010 + +COGS - Accessories + +Cost of accessories sold + +5020 + +COGS - Supplies + +Cost of supplies sold + +5100 + +Repair Parts Cost + +Cost of parts used in repairs + + + +## 2.6 Expenses + +Code + +Account Name + +Notes + +6000 + +Cash Over / Short + +Drawer variance — over is credit, short is debit + +6100 + +Payment Processing Fees + +Stripe transaction fees + +6200 + +Bad Debt Expense + +Written-off account balances + +6300 + +Inventory Shrinkage + +Inventory adjustments and write-offs + + + +# 3. Database Schema + +## 3.1 account_code + +Store-configurable chart of accounts. Each store maps platform account codes to their QuickBooks account names and numbers. + +id, company_id, code (varchar), name (varchar),account_type (enum: asset|liability|revenue|contra_revenue|cogs|expense),quickbooks_account_name (varchar), ← must match QB exactly for importquickbooks_account_number (varchar),is_active (boolean), created_at + + + +## 3.2 journal_entry + +Header record for each accounting event. One journal entry per financial event — groups all debit and credit lines for that event. + +Column + +Type + +Notes + +id + +uuid PK + + + +company_id + +uuid FK + + + +entry_number + +varchar + +Human-readable JE number e.g. JE-2024-00142 + +entry_date + +date + +Accounting date — may differ from created_at + +entry_type + +enum + +See section 4 for full list + +source_entity_type + +enum + +transaction|rental|enrollment|repair|drawer_session|batch + +source_entity_id + +uuid + +FK to the originating record + +description + +text + +Human-readable description for accountant + +total_debits + +numeric(10,2) + +Must equal total_credits + +total_credits + +numeric(10,2) + +Must equal total_debits + +export_batch_id + +uuid + +Set when entry is included in an export + +exported_at + +timestamptz + +When exported — null if not yet exported + +reconciled_at + +timestamptz + +When accountant confirmed import to QB + +is_void + +boolean + +Voided entries excluded from exports + +void_reason + +text + +Required if voided + +created_by + +uuid FK + +System or employee + +created_at + +timestamptz + + + + + +## 3.3 journal_entry_line + +Individual debit and credit lines for each journal entry. Every entry must have at least one debit and one credit line and total_debits must equal total_credits. + +Column + +Type + +Notes + +id + +uuid PK + + + +journal_entry_id + +uuid FK + + + +account_code_id + +uuid FK + +Which account is affected + +line_type + +enum + +debit | credit + +amount + +numeric(10,2) + +Always positive — line_type determines direction + +description + +text + +Line-level description + +created_at + +timestamptz + + + + + +## 3.4 export_batch + +Records each export operation for audit and reconciliation tracking. + +id, company_id, exported_by (uuid FK), export_format (csv|iif),date_range_from, date_range_to, entry_count, total_amount,file_path (S3 URL), reconciled_at, created_at + + + +# 4. Journal Entry Mappings + +Every financial event generates one or more journal entries automatically. The following tables show the exact debit and credit mapping for each event type. All entries are generated by the platform — staff do not create journal entries manually. + + + +## 4.1 Cash Sale + +Debit + +Credit + +Notes + +1000 Cash - Store Drawer + +4000-4020 Sales Revenue + +Revenue recognized at point of sale + +1000 Cash - Store Drawer + +2000 Sales Tax Payable + +Tax collected from customer + +5000-5020 COGS + +1300 Inventory - Sale Stock + +Cost of item removed from inventory + + + +## 4.2 Card Sale (Stripe Terminal or Keyed) + +Debit + +Credit + +Notes + +1200 Stripe Clearing + +4000-4020 Sales Revenue + +Card payment in transit to bank + +1200 Stripe Clearing + +2000 Sales Tax Payable + +Tax collected + +5000-5020 COGS + +1300 Inventory - Sale Stock + +Cost of item sold + + + +When Stripe pays out to bank account (triggered by Stripe payout webhook): + +Debit + +Credit + +Notes + +1000 Cash - Store Drawer + +1200 Stripe Clearing + +Funds settled from Stripe to bank + +6100 Payment Processing Fees + +1200 Stripe Clearing + +Stripe fee deducted from payout + + + +## 4.3 Discount Applied at POS + +Debit + +Credit + +Notes + +4900 Sales Discounts + +4000-4020 Sales Revenue + +Contra revenue — keeps gross revenue clean + + + +Note: Gross revenue is always recorded at full price. The discount is recorded separately as a contra-revenue debit. Net revenue = Sales Revenue minus Sales Discounts. This gives the accountant visibility into both gross and net figures. + + + +## 4.4 Sale Refund + +Debit + +Credit + +Notes + +4910 Sales Returns & Refunds + +1000/1200 Cash or Stripe Clearing + +Refund issued to customer + +1300 Inventory - Sale Stock + +5000-5020 COGS + +Item returned to inventory at cost + +2000 Sales Tax Payable + +1000/1200 Cash or Stripe Clearing + +Tax refunded + + + +## 4.5 Rental Payment Received + +Standard month-to-month rental — full payment is earned revenue: + +Debit + +Credit + +Notes + +1200 Stripe Clearing + +4100 Rental Revenue + +Monthly rental payment — earned immediately + +1200 Stripe Clearing + +2000 Sales Tax Payable + +Tax if applicable to rentals in jurisdiction + + + +## 4.6 Rent-to-Own Payment Received + +RTO payments split between earned rental revenue and deferred equity portion: + +Debit + +Credit + +Notes + +1200 Stripe Clearing + +4100 Rental Revenue + +Rental portion — earned immediately + +1200 Stripe Clearing + +2120 Deferred Revenue - RTO Equity + +Equity portion — not earned until buyout + + + +## 4.7 Rent-to-Own Buyout Completed + +When customer exercises buyout option — deferred equity is recognized as revenue: + +Debit + +Credit + +Notes + +2120 Deferred Revenue - RTO Equity + +4400 RTO Buyout Revenue + +Accumulated equity recognized on buyout + +1200/1000 Stripe or Cash + +4400 RTO Buyout Revenue + +Remaining buyout balance collected + +5000 COGS - Instruments + +1310 Inventory - Rental Fleet + +Cost of instrument transferred from fleet to sold + + + +## 4.8 Rental Security Deposit Collected + +Deposit is a liability — not revenue until earned or applied: + +Debit + +Credit + +Notes + +1000/1200 Cash or Stripe Clearing + +2200 Rental Deposits Held + +Deposit received — held as liability + + + +## 4.9 Rental Deposit Returned + +Debit + +Credit + +Notes + +2200 Rental Deposits Held + +1000/1200 Cash or Stripe Clearing + +Deposit refunded to customer on return + + + +## 4.10 Rental Deposit Kept (Damage) + +Debit + +Credit + +Notes + +2200 Rental Deposits Held + +4500 Other Income + +Deposit applied to damage — now earned income + + + +## 4.11 Lesson Payment Received + +Debit + +Credit + +Notes + +1200 Stripe Clearing + +4200 Lesson Revenue + +Monthly lesson payment — earned for current month + + + +Note: If the store policy is to recognize lesson revenue per session delivered rather than per month billed, the entry on payment would credit Deferred Revenue - Lessons (2110) and recognize revenue as each session is marked attended. Most small music stores bill monthly and recognize on billing — either approach is acceptable but must be consistent. + + + +## 4.12 Makeup Lesson Credit Issued + +Debit + +Credit + +Notes + +4200 Lesson Revenue + +2300 Customer Credits + +Revenue reversed for undelivered lesson + + + +## 4.13 Repair Payment Collected at Pickup + +Debit + +Credit + +Notes + +1000/1200 Cash or Stripe Clearing + +4300 Repair Revenue - Labor + +Labor charges collected + +1000/1200 Cash or Stripe Clearing + +4310 Repair Revenue - Parts + +Parts charges collected + +1000/1200 Cash or Stripe Clearing + +2000 Sales Tax Payable + +Tax on parts if applicable + +5100 Repair Parts Cost + +1320 Inventory - Parts & Supplies + +Cost of parts consumed in repair + + + +## 4.14 Repair Charged to Account + +Debit + +Credit + +Notes + +1100 Accounts Receivable + +4300 Repair Revenue - Labor + +Repair billed to account — not yet paid + +1100 Accounts Receivable + +4310 Repair Revenue - Parts + +Parts billed to account + +5100 Repair Parts Cost + +1320 Inventory - Parts & Supplies + +Cost of parts consumed + + + +## 4.15 Account Payment Received + +Debit + +Credit + +Notes + +1000/1200 Cash or Stripe Clearing + +1100 Accounts Receivable + +Payment applied to outstanding balance + + + +## 4.16 Bad Debt Write-Off + +Debit + +Credit + +Notes + +6200 Bad Debt Expense + +1100 Accounts Receivable + +Uncollectible balance written off — requires manager approval + + + +## 4.17 Billing Date Change — Proration Credit + +Debit + +Credit + +Notes + +4920 Proration Credits Issued + +1100/1200 AR or Stripe Clearing + +Credit issued to customer for billing date shift + + + +## 4.18 Billing Date Change — Proration Charge + +Debit + +Credit + +Notes + +1200 Stripe Clearing + +4100/4200 Rental or Lesson Revenue + +Additional charge for extended billing period + + + +## 4.19 Inventory Purchase (Receiving PO) + +Debit + +Credit + +Notes + +1300 Inventory - Sale Stock + +1100 Accounts Payable + +Merchandise received — owed to vendor + +1310 Inventory - Rental Fleet + +1100 Accounts Payable + +Rental fleet instruments received + + + +## 4.20 Vendor Payment + +Debit + +Credit + +Notes + +1100 Accounts Payable + +1000/1200 Cash or Bank + +Payment to vendor for purchase order + + + +## 4.21 Cash Drawer — Opening Float + +Debit + +Credit + +Notes + +1000 Cash - Store Drawer + +1000 Cash - Store Drawer + +Internal transfer — float set for shift (net zero) + + + +## 4.22 Cash Drawer — Closing Variance + +If drawer is over (more cash than expected): + +Debit + +Credit + +Notes + +1000 Cash - Store Drawer + +6000 Cash Over / Short + +Overage — credit to over/short account + + + +If drawer is short (less cash than expected): + +Debit + +Credit + +Notes + +6000 Cash Over / Short + +1000 Cash - Store Drawer + +Shortage — debit to over/short account + + + +## 4.23 Cash Drop Mid-Shift + +Debit + +Credit + +Notes + +1000 Cash - Safe / Vault + +1000 Cash - Store Drawer + +Cash moved from drawer to secure location + + + +## 4.24 Batch Repair Invoice — School + +Batch repair invoices follow the same pattern as individual repair payments but reference the batch record: + +Debit + +Credit + +Notes + +1100 Accounts Receivable + +4300 Repair Revenue - Labor + +Batch labor billed to school account + +1100 Accounts Receivable + +4310 Repair Revenue - Parts + +Batch parts billed to school account + +5100 Repair Parts Cost + +1320 Inventory - Parts & Supplies + +Cost of all parts used across batch + + + +# 5. Journal Entry Validation + +Every journal entry is validated before being written to the database. Validation failures block the originating financial event and alert staff. + +- total_debits must equal total_credits — entries that do not balance are rejected + +- Every line must reference a valid active account_code + +- Entry date cannot be more than 30 days in the past without manager override + +- Entry date cannot be in the future + +- Voided entries generate a reversing entry — equal and opposite lines — rather than being deleted + +- Reversing entries reference the original entry ID for traceability + + + +# 6. QuickBooks Export + +## 6.1 Export Format + +Journal entries are exported as CSV in a format compatible with QuickBooks Desktop and QuickBooks Online import. Each row represents one journal entry line. + + + +CSV Column + +Source + +Date + +journal_entry.entry_date + +Journal No + +journal_entry.entry_number + +Account + +account_code.quickbooks_account_name — must match QB exactly + +Debit + +journal_entry_line.amount where line_type = debit + +Credit + +journal_entry_line.amount where line_type = credit + +Description + +journal_entry.description + +Name + +account.name where applicable — for AR/AP lines + +Class + +Optional — store name for multi-location QB setups + + + +## 6.2 Export Workflow + +- Staff or accountant selects date range for export + +- System returns count of unexported entries in range for confirmation + +- Export generated as CSV and stored in S3 — download link provided + +- All included entries marked with export_batch_id and exported_at timestamp + +- Entries already exported are excluded from subsequent exports by default + +- Override option available to re-export a specific batch — requires manager approval + +- Accountant imports CSV into QuickBooks — confirms successful import in platform + +- Confirmed entries marked reconciled_at — visible in reconciliation report + + + +## 6.3 Export Frequency Recommendation + +Daily export is recommended for active stores to keep QuickBooks current. Weekly export is acceptable for lower-volume stores. Monthly export creates large import files and increases risk of errors going undetected. The platform supports any frequency — the accountant decides what works for their workflow. + + + +# 7. In-Platform Accounting Reports + +These reports are generated from journal entry data within the platform and do not require QuickBooks. They give the store owner operational financial visibility without waiting for the accountant's monthly close. + + + +Report + +Description + +Revenue Summary + +Revenue by category (instruments, rentals, lessons, repairs) by period + +Gross vs Net Revenue + +Gross revenue vs discounts vs net — shows discount impact + +AR Aging + +Outstanding account balances by age — current, 30, 60, 90+ days + +Deferred Revenue + +RTO equity, lesson credits, deposits held — future obligations + +Cash Flow Summary + +Cash in vs out by period — drawer, Stripe, deposits + +Stripe Clearing Reconciliation + +Amounts in clearing vs expected payouts — flags missing payouts + +Export Status + +Entries exported vs pending export — reconciliation tracking + +Cash Over / Short History + +Drawer variance by shift, employee, and register + +COGS vs Revenue + +Gross margin by product category + + + +# 8. Business Rules + +- Journal entries are generated automatically — staff never create entries manually + +- Every financial event must produce a balanced journal entry before the event is committed + +- Entries are immutable — corrections made via reversing entries only + +- Voided entries generate equal and opposite reversing entries — never deleted + +- Exported entries are not re-exported unless explicitly overridden with manager approval + +- Chart of accounts is configurable per store — account names must match QuickBooks exactly + +- Stripe clearing account must be reconciled against Stripe payout reports monthly + +- Deferred revenue accounts reviewed quarterly — amounts that should have been recognized flagged + +- Bad debt write-offs require manager approval and are logged in audit trail + +- AIM migration historical transactions imported as journal entries with legacy_source tag — clearly separated from live entries in exports \ No newline at end of file diff --git a/planning/13_Deployment_Compliance_Pricing_Support.docx b/planning/13_Deployment_Compliance_Pricing_Support.docx new file mode 100644 index 0000000000000000000000000000000000000000..3d56bc61f2370065cf64cbb79f0e07e8037fe05c GIT binary patch literal 21984 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∈>*)+{Jh#zDEgXdTJ@1v2^l_`>dwDY z&v6`o;iviCuOuw-mgc7gJe36!(Ql_`*GzLeSl0GMK6cUeHrc3v8q4+U-x=AW#2y%a zdpafM?2|vqNfYHSP82LWVR7tG_Su`Ati9bImV13(wBzX0%!cmm4EN_;pVR)pYX5~V zm0R5J$V5EeZP&Op|I_K!M?asj+4|rmYgC|}*$XBc=^_{1(<@%x{1Lsm@R5h?-p04) z0<+t)8jGeMJ3ir+=)Hx1bf5aOPFfaeHpO;E*VTkYNxO|NPl+h=zsxY(Uu=5MHjZga z;(4dU`>is((kkp_=k>{Tvi|2!&(M-xcbnyrqrmt)25eix<1;xw7j&8lBtEa)EqbIN z!S*1!&?0(}J|O6C6K>DLb&zU*nz zt!z3?JF>ZEY9s5^2tmE?rWN(S_ugr0E%gw(qT!icuuROx)^3Yvm{Y*ZH;0%nh5ZO& zel};F!j>6dPa;Ah@Z~ye&Y3uDmA4S6zU(Iwj{ozY$>shRRLHcJ-3UmFzTpm%* zfO!X%`d5C_B^^*?>E-{(3?g)_wG6qZ-1y|=B9tAPi#Kao@ZUZRz~X954|Z{da`d{pSkL1w~oT``C^44zB|@m z{dwf=B`xP&`!=o5zop@`_N%O2{3@9PnjhEy?S3}tPrBiGwLi+T7yW%YKm47k*!dy< z;fDuHvfQ3N6Xu`5AR-u$WcBd?|FLgb4mNKtoO>v|-L2r%y@x4Jmb6MTPO|_mA9sBr zTDJLwVw;Y1cS+m|-}7^iu29~)KUaB=MCpa*WBHfk+LPB#{VX

i=!)-sUF$GgzY8 z&|i;sC<~sV207*jx~NHh8+T0>m^7V%L&Qk!w7im##H1YXP}1Csck>P@h_HVMSG=RW zb!KnontM9lW&3Rzn6|p`Er0mn%69p++p-_^-W3)2^#5s1&iC+wH^*5vz0lqll{QCm zeL!^mnjhbH2bO$qV13`E5Vyw(p%{`pZLSsqNGSTKA6JnC3Jgi^n4>DZA#^ z8pDR3my&%o8{Zef3&v!~PMG}pg`L)u)|wuU~Fy(-z$UmM5rcv8nasoR$p z#Kv9JlPHl6kX&@daK~=;OTQ*Y?zMGH2z5AQr59}6-&Lh`G(57N`8)fX=Zrsv12l@~ zrsv4Dd7NCC{MBZ;gzRedbN{<$GAvn~m!|pCApBx#>XhCkXokb-m|BAt;*gaP-FO~5u95P*6bAOJnU z3AYx|cmV`}v?v2HQVTD@8>SJ`7(_qP391uF`Jie@7g z{1Ku3wi(pPD5H@G{g|!>wsD~VAj%qM=tU~ zy&eP*a)9c`)dxa13cX{3FlrafDC7n!x^`rjfZDkTu*wO=CCG+gjZ^fdD8i&bXQ)ZY z4O4W7ptpPw+7(@)+L3ERbnUqPA?l9e4`f5I`UAb5LpWp)%%n8nfFHcp!{-vzxaIUj baS4hkP^Ykhvz{1(7()%PMZXcKfq?-4v;m_o literal 0 HcmV?d00001 diff --git a/planning/14_Self_Hosted_Installer_Platform_Compatibility.md b/planning/14_Self_Hosted_Installer_Platform_Compatibility.md new file mode 100644 index 0000000..25e528f --- /dev/null +++ b/planning/14_Self_Hosted_Installer_Platform_Compatibility.md @@ -0,0 +1,653 @@ +Music Store Management Platform + +Self-Hosted Deployment — Installer Design & Platform Compatibility + +Version 1.0 | Draft + + + +# 1. Overview + +The self-hosted deployment must be as simple as installing any consumer application. The target user is a music store owner or manager with no IT background. The experience should match what they expect from installing any Windows or Mac application — download, double-click, next-next-finish, done. + + + +Under the hood the platform runs in Docker containers managed by Docker Compose. The store owner never sees Docker, never opens a terminal, and never touches a config file. All of that complexity is hidden inside a native installer and a system tray management app. + + + +User sees + +What actually happens + +Download setup.exe + +Downloads a bundled installer containing Docker, your stack, and the tray app + +Double-click installer + +Installer checks Windows edition, enables Hyper-V or WSL2, installs Docker Engine + +Fill in store name + password + +Generates .env config file with store settings and secure random secrets + +Click Install + +Docker Compose pulls images, initializes Postgres, seeds config, registers Windows service + +Browser opens automatically + +Nginx serves the app at localhost:3000 — first-run setup wizard shown + +Tray icon appears + +Management app running — monitors service health, handles updates, backup triggers + + + +# 2. Virtualization Layer + +Docker requires a Linux kernel to run containers. On Windows and Mac, a lightweight virtualization layer provides this. The installer detects and configures the appropriate layer automatically based on the OS. + + + +## 2.1 Windows Virtualization + +Windows Edition + +Virtualization + +Installer Action + +Windows 10/11 Pro + +Hyper-V + +Enable Hyper-V via PowerShell silently — no user action required + +Windows 10/11 Enterprise + +Hyper-V + +Same as Pro — Hyper-V included and licensable + +Windows 10/11 Home + +WSL2 + +Install WSL2 silently. If BIOS virtualization disabled, show friendly guide. + +Windows Server 2019/2022 + +Hyper-V + +Docker Engine direct — no Desktop needed. Most performant option. + + + +The vast majority of music store computers will be Windows 10/11 Pro — retail businesses typically purchase Pro licenses. Home edition is handled gracefully as a fallback. Windows Server is supported for IT-managed deployments. + + + +### Hyper-V Silent Enable (PowerShell) + +# Installer runs this silently — requires admin elevationEnable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -NoRestartEnable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart# Reboot required after enabling Hyper-V# Installer schedules resume-after-reboot via registry run key + + + +## 2.2 macOS Virtualization + +macOS 12+ (Monterey and later) includes Apple's Virtualization Framework built into the OS. Docker Desktop uses this transparently. No additional configuration required from the store owner. + + + +macOS Version + +Architecture + +Notes + +macOS 12+ (Monterey+) + +Intel x86-64 + +Apple Virtualization Framework — fully supported + +macOS 12+ (Monterey+) + +Apple Silicon ARM64 + +Native ARM containers — best performance on M1/M2/M3 + +macOS 11 (Big Sur) + +Intel only + +Supported but approaching end of life — recommend upgrade + +macOS 10.x or earlier + +Not supported + +Too old — installer shows friendly upgrade message + + + +## 2.3 Linux + +Linux deployments run Docker Engine natively — no virtualization layer required. Containers run directly on the host kernel. This is the most performant deployment option and is appropriate for IT-managed stores or dedicated server hardware. + + + +# One-line install for Ubuntu 22/24 LTScurl -fsSL https://install.platform.com/linux | bash# Script handles:# - Docker Engine installation# - Docker Compose installation# - Stack configuration# - Systemd service registration# - First-run database initialization + + + +# 3. Platform Compatibility Matrix + +Platform + +Supported + +Virtualization + +Architecture + +Notes + +Windows 11 Pro/Enterprise + +Yes + +Hyper-V + +x86-64 + +Recommended for most stores + +Windows 11 Home + +Yes + +WSL2 + +x86-64 + +BIOS virt must be enabled + +Windows 10 Pro/Enterprise + +Yes + +Hyper-V + +x86-64 + +Version 1903+ required + +Windows 10 Home + +Yes + +WSL2 + +x86-64 + +May need BIOS change + +Windows Server 2022 + +Yes + +Hyper-V + +x86-64 + +Docker Engine direct + +Windows Server 2019 + +Yes + +Hyper-V + +x86-64 + +Docker Engine direct + +macOS 14 Sonoma + +Yes + +Apple Virt FW + +ARM64 / x86-64 + +Intel and Apple Silicon + +macOS 13 Ventura + +Yes + +Apple Virt FW + +ARM64 / x86-64 + + + +macOS 12 Monterey + +Yes + +Apple Virt FW + +x86-64 + +Intel only + +macOS 11 Big Sur + +Limited + +HyperKit + +x86-64 + +Approaching EOL + +Ubuntu 22.04 LTS + +Yes + +Native + +x86-64 / ARM64 + +Recommended Linux + +Ubuntu 24.04 LTS + +Yes + +Native + +x86-64 / ARM64 + + + +Raspberry Pi (4/5) + +Yes + +Native + +ARM64 + +Pi 5 recommended — 8GB RAM + + + +Multi-architecture Docker images are built for both linux/amd64 and linux/arm64 on every release. Docker automatically pulls the correct image for the host architecture. No platform-specific configuration required from the store. + + + +## 3.1 Minimum Hardware Requirements + +Component + +Minimum + +Recommended + +CPU + +4-core x86-64 or ARM64 + +6-core or better — handles concurrent POS terminals smoothly + +RAM + +8GB + +16GB — headroom for Postgres query cache and API under load + +Storage + +100GB SSD + +500GB SSD — 5+ years of transaction data with room to spare + +Network + +100Mbps LAN + +Gigabit LAN — fast for multi-terminal stores + +OS + +See compatibility matrix + +Windows 11 Pro or Ubuntu 24 LTS + + + +## 3.2 Recommended Hardware + +For most music stores a mini PC is the ideal self-hosted server. Small, quiet, low power, no moving parts, runs 24/7 reliably. Examples: + + + +Device + +Est. Cost + +Notes + +Intel NUC 13 Pro (i5) + +$400-500 + +Compact, reliable, Windows 11 Pro. Popular choice for small business servers. + +Beelink Mini S12 Pro + +$180-220 + +Budget option. N100 processor handles the stack well. 16GB RAM config recommended. + +Existing store PC + +$0 + +If meets min specs — install alongside existing OS. Avoids new hardware purchase. + +Raspberry Pi 5 (8GB) + +$80-120 + +ARM64, fanless, extremely low power. Requires Ubuntu — Linux-savvy stores only. + +Dell OptiPlex (refurb) + +$150-250 + +Refurbished business desktop. Reliable, widely available, Windows Pro pre-installed. + + + +# 4. Installer Design + +## 4.1 Windows Installer (.exe) + +Built with Inno Setup or NSIS — industry standard Windows installer frameworks. Produces a single signed .exe that handles the complete installation. Code signing certificate required to avoid Windows SmartScreen warnings. + + + +### Installer Contents + +setup.exe (self-extracting archive) contains: ├── docker-install/ Docker Desktop or Engine MSI ├── compose/ │ ├── docker-compose.yml │ ├── nginx.conf │ └── init-db.sql ├── tray-app/ │ └── PlatformManager.exe (Electron or Go tray app) ├── scripts/ │ ├── install.ps1 PowerShell install logic │ ├── enable-hyperv.ps1 Hyper-V activation │ ├── service-register.ps1 Windows service setup │ └── first-run.ps1 Initial DB seed └── resources/ ├── icon.ico └── license.txt + + + +### Installation Steps (User Facing) + +Screen + +Title + +What happens + +1 + +Welcome + +Intro screen, version number, Next button + +2 + +License Agreement + +EULA — must accept to continue + +3 + +Install Location + +Default: C:\Program Files\Platform — most stores leave default + +4 + +Store Setup + +Store name, admin email, admin password, timezone — generates .env + +5 + +Installing + +Progress bar — enables Hyper-V/WSL2, installs Docker, pulls images, seeds DB + +6 + +Complete + +Open Platform button — launches browser to localhost:3000 + + + +### Behind the Scenes During Install Step 5 + +1. Check Windows edition → select Hyper-V or WSL22. Enable virtualization layer (may require reboot — handled automatically)3. Install Docker Desktop silently (--quiet flag)4. Wait for Docker daemon to start (poll /var/run/docker.sock)5. Pull Docker images (progress shown per image)6. Generate SSL certificate (self-signed, 10yr validity)7. Write .env file with store config and generated secrets8. docker compose up -d9. Wait for Postgres health check to pass10. Run database migrations11. Seed initial config (store name, admin user, chart of accounts)12. Register PlatformManager.exe as Windows startup item13. Register docker compose as Windows service (auto-start on boot)14. Open browser to localhost:3000 + + + +## 4.2 macOS Installer (.dmg) + +Standard macOS disk image containing a drag-to-Applications app bundle. The app bundle is a macOS app that includes Docker Desktop and manages the compose stack. Code signed and notarized with Apple Developer certificate to avoid Gatekeeper warnings. + + + +Platform.dmg └── Platform.app (drag to /Applications) └── Contents/ ├── MacOS/ │ └── PlatformManager (menu bar app binary) ├── Resources/ │ ├── docker-compose.yml │ ├── Docker.dmg (Docker Desktop bundled) │ └── icon.icns └── Info.plist + + + +On first launch the menu bar app detects if Docker Desktop is installed, installs it if not, pulls images, and starts the stack. Subsequent launches just start the menu bar icon — stack starts automatically via launchd. + + + +## 4.3 Linux Install Script + +Single bash script handles the complete Linux installation. Appropriate for IT-managed deployments and technically capable store owners on Ubuntu. + +curl -fsSL https://install.platform.com/linux | sudo bash# Script actions:# 1. Detect distro (Ubuntu 22/24 supported)# 2. Install Docker Engine via official apt repo# 3. Install Docker Compose plugin# 4. Create /opt/platform/ directory# 5. Download docker-compose.yml and config files# 6. Prompt for store name, admin email, password# 7. Generate .env and SSL cert# 8. docker compose up -d# 9. Register systemd service for auto-start# 10. Print access URL and admin credentials + + + +# 5. System Tray & Menu Bar Management App + +The management app is the store owner's window into the platform's health. It runs silently in the background and provides a simple interface for the most common management tasks without requiring any technical knowledge. + + + +## 5.1 Tech Stack + +The tray app is built as a small standalone binary using one of the following approaches — final choice based on development effort vs capability tradeoff: + + + +Option + +Size + +Notes + +Go + systray lib + +~10MB + +Recommended — small binary, cross-platform, easy Docker API calls, fast startup + +Rust + tray-icon + +~5MB + +Smallest binary, great performance, steeper development curve + +Electron + +~150MB + +Easiest to build, heaviest — uses same stack as desktop app so reusable code + +Python + pystray + +~50MB + +Quick to develop but large bundled size + + + +## 5.2 Tray Icon States + +Icon State + +Color + +Meaning + +Running + +Green + +All services healthy — API, Postgres, Redis all responding + +Starting + +Yellow + +Services starting up — normal after boot, takes 30-60 seconds + +Update available + +Blue badge + +New version available — click to view changelog and update + +Warning + +Orange + +One service degraded but platform partially operational + +Stopped / Error + +Red + +Platform not running or critical service down + + + +## 5.3 Tray Menu + +Right-click tray icon: ┌─────────────────────────────┐ │ ● Platform Manager │ │ Version 1.5.2 │ ├─────────────────────────────┤ │ Open Platform → │ Opens localhost:3000 in browser ├─────────────────────────────┤ │ Status │ │ ✓ API Running │ │ ✓ Database Running │ │ ✓ Cache Running │ ├─────────────────────────────┤ │ 🔵 Update Available (1.6.0) │ Shows when update exists │ View Changelog │ │ Install Update │ ├─────────────────────────────┤ │ Backup Now │ Triggers manual backup │ View Last Backup │ Shows last backup time ├─────────────────────────────┤ │ Restart Services │ docker compose restart │ View Logs │ Shows last 100 API log lines ├─────────────────────────────┤ │ Stop Platform │ docker compose down └─────────────────────────────┘ + + + +## 5.4 Update Flow via Tray App + +1. Tray app polls update registry daily on startup GET https://updates.platform.com/check?version=1.5.2&key=LICENSE2. If update available — tray icon shows blue badge Tooltip: 'Platform 1.6.0 available'3. Store owner right-clicks → View Changelog Simple window shows what's new in plain language 'Fixed rental billing date issue, improved school batch reports'4. Store owner clicks Install Update Progress window appears: [=====> ] Backing up database... [=========> ] Downloading update... [=============>] Applying update... [==============] Done!5. Browser refreshes automatically — new version running If update fails — automatic rollback to previous version Error message: 'Update failed, your previous version has been restored' + + + +# 6. Auto-Start & Service Management + +The platform starts automatically when the server boots. Store owners should never need to manually start the software — it should just be running when they arrive in the morning. + + + +## 6.1 Windows Service + +Platform Manager registered as Windows Service: Name: PlatformService Startup: Automatic (delayed) Recovery: Restart on failure (3 attempts) Action: Runs 'docker compose up -d' on startDocker Desktop also set to start with Windows.Sequence on boot: 1. Windows starts 2. Docker Desktop starts (Hyper-V/WSL2 VM initializes) 3. PlatformService starts (waits for Docker daemon) 4. docker compose up -d (starts all containers) 5. Tray app launches (shows starting state) 6. ~60 seconds after boot — platform ready + + + +## 6.2 macOS launchd + +LaunchAgent plist registered in ~/Library/LaunchAgents/ com.platform.manager.plist RunAtLoad: true KeepAlive: trueSame sequence as Windows — Docker starts, then compose stack. + + + +## 6.3 Linux systemd + +# /etc/systemd/system/platform.service[Unit]Description=Platform Music StoreAfter=docker.serviceRequires=docker.service[Service]WorkingDirectory=/opt/platformExecStart=docker compose upExecStop=docker compose downRestart=alwaysRestartSec=10[Install]WantedBy=multi-user.target + + + +# 7. Backup — Self-Hosted + +Backup is the store's responsibility on self-hosted deployments. The platform provides automated local backup tooling and an optional cloud backup add-on. Store owners should be clearly informed during installation that they are responsible for their own data. + + + +## 7.1 Local Backup (Included) + +- Automated daily Postgres dump to local backup directory + +- Default location: install directory /backups/ + +- Retains last 30 daily backups — older automatically pruned + +- Backup integrity verified after each dump — corrupt backup flagged in tray app + +- Tray app shows last backup time and status + +- Manual backup available via tray menu at any time + +- Store strongly recommended to copy backups to external drive or NAS regularly + + + +## 7.2 Cloud Backup Add-On ($29/month) + +- Encrypted backup uploaded to vendor S3 after each local backup + +- Encryption key held by customer — vendor cannot read backup data + +- 30-day retention in cloud — point-in-time restore available + +- Restore wizard available in admin panel — no technical knowledge required + +- Ideal for stores without IT staff or dedicated backup infrastructure + + + +## 7.3 Backup Responsibility Notice + +During installation the store owner must acknowledge the following before completing setup: + +IMPORTANT — DATA BACKUP RESPONSIBILITYYou are responsible for backing up your data.This software includes automated local backups but theseare stored on the same machine as your data.We strongly recommend: • Enabling cloud backup ($29/month add-on), OR • Regularly copying backups to an external drive or NASHardware failure can result in permanent data lossif backups are not stored separately.[ ] I understand I am responsible for my data backups (checkbox — must check to proceed) + + + +# 8. Network Access + +The platform runs on the store's local network. The Electron desktop app, iOS app, and any browser on the local network can access it by IP address or hostname. + + + +- Default access: http://[server-ip]:3000 or http://platform.local (mDNS) + +- HTTPS available with self-signed cert — browser warning expected, staff instructed to accept + +- Optional Let's Encrypt cert if store has a domain name pointed at their server + +- Remote access via VPN — store uses existing VPN infrastructure + +- No ports need to be opened to the internet for core operation + +- Cloud backup and update checker are the only outbound internet connections required + + + +For stores that want remote access without a VPN, Cloudflare Tunnel is a free option that creates a secure tunnel without exposing ports. The tray app can optionally configure this during setup. \ No newline at end of file diff --git a/planning/15_Licensing_Modules_Pricing.docx b/planning/15_Licensing_Modules_Pricing.docx new file mode 100644 index 0000000000000000000000000000000000000000..11c5cec71543becf96e985160cde1b9d1883045c GIT binary patch literal 19614 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 literal 0 HcmV?d00001 diff --git a/planning/15_Licensing_Modules_Pricing.md b/planning/15_Licensing_Modules_Pricing.md new file mode 100644 index 0000000..9b6ff23 --- /dev/null +++ b/planning/15_Licensing_Modules_Pricing.md @@ -0,0 +1,823 @@ +Music Store Management Platform + +Licensing, Module Structure & Pricing + +Version 1.0 | Draft + + + +# 1. Licensing Philosophy + +The platform offers two fundamentally different ownership models depending on deployment type. Self-hosted customers can own their software outright — pay once, use forever. SaaS customers subscribe to a hosted service. This distinction maps cleanly to how customers think about each deployment type. + + + + + +Self-Hosted + +SaaS + +Model + +Perpetual License + +Subscription + +Payment + +One-time upfront per module + +Monthly or annual recurring + +Software access + +Forever — software never stops working + +While subscription is active + +Updates + +While maintenance is active + +Always included + +Support + +While maintenance is active + +Always included per plan tier + +Hosting + +Customer's own hardware + +Vendor managed on AWS + +Best for + +Stores that want ownership, AIM migrants, IT-managed + +Stores that want zero IT burden, cloud-first + + + +# 2. Module Structure + +The platform is built around a core that is always included, with optional modules that unlock additional functionality. Each module is licensed independently — stores pay only for what they use. Module licenses are perpetual for self-hosted customers and included in subscription tiers for SaaS customers. + + + +## 2.1 Core (Always Included) + +Core is included with every license at no additional cost. It is never disabled, never expires, and requires no ongoing payment. A store can always process sales even if maintenance lapses or a subscription expires. + + + +CORE — Always Included + +Point of Sale — cash, card present, card keyed, account charge + +Basic inventory — products, categories, stock levels, low stock alerts + +Customer management — accounts, contacts, basic history + +Cash drawer — sessions, denominations, over/short tracking + +User accounts and role-based access control + +Basic sales reporting — daily, weekly, monthly totals + +Invoice and receipt printing + +One payment processor module required (PAY-STRIPE or PAY-GP) + + + +## 2.2 Licensed Modules + +Module ID + +Description + +Typical User + +MOD-RENTALS + +Instrument rentals — month-to-month, rent-to-own, short-term, lease purchase. Rental fleet inventory, deposit tracking, return workflow. + +Most music stores + +MOD-LESSONS + +Lesson scheduling — instructors, enrollments, schedule slots, attendance, makeup credits, lesson billing. + +Stores offering lessons + +MOD-REPAIRS + +Repair ticket management — intake, diagnosis, estimates, approval, labor and parts tracking, customer notification. + +Most music stores + +MOD-BATCH + +Batch repairs for schools and institutions — bulk intake, group approval, batch invoicing. Requires MOD-REPAIRS. + +School music dealers + +MOD-DELIVERY + +Delivery and chain of custody tracking — pickup scheduling, per-instrument condition, signature capture. Requires MOD-BATCH. + +School music dealers + +MOD-ACCOUNTING + +Journal entry generation, chart of accounts, QuickBooks CSV export, AR aging, deferred revenue tracking. + +Stores needing accounting + +MOD-BILLING + +Advanced billing management — billing date changes, proration, consolidated family billing, split billing, bulk date changes. + +Stores with lessons/rentals + +MOD-MOBILE + +iOS convention app — mobile POS, Bluetooth Terminal, customer lookup, inventory check, offline sync. + +Convention sellers + +MOD-PORTAL + +Customer self-service portal — lesson billing management, repair status tracking, invoice history, online payment. + +Customer-facing stores + +MOD-SCHOOL + +School and district management — district lists, instrument categories, school contacts, school-specific rental packages. + +School music dealers + +MOD-MULTI + +Multi-location support — additional store locations, cross-location inventory visibility, consolidated reporting. + +Multi-location stores + +MOD-API + +API access — REST API for third-party integrations, webhooks, data export. Rate limited by tier. + +Technical integrations + + + +## 2.3 Payment Processor Modules + +Exactly one payment processor module is required. The store uses their own processor account and credentials — the platform does not handle merchant accounts or transaction fees. + + + +Module ID + +Description + +Notes + +PAY-STRIPE + +Stripe integration — Terminal (WiFi + Bluetooth), Elements for keyed entry, Subscriptions, Webhooks, Card Updater. + +Recommended default + +PAY-GP + +Global Payments integration — Terminal hardware, recurring billing, tokenization, Decline Minimizer. Supports token import from AIM. + +For GP stores + + + +Additional processor modules may be added in future releases. The payment abstraction layer architecture supports adding new processors without changes to business logic. + + + +## 2.4 Module Dependencies + +Module + +Requires + +MOD-BATCH + +MOD-REPAIRS — batch repairs extend individual repair functionality + +MOD-DELIVERY + +MOD-BATCH — delivery tracking is part of the batch repair workflow + +MOD-SCHOOL + +MOD-RENTALS + MOD-BATCH — school module extends both + +MOD-BILLING + +MOD-RENTALS or MOD-LESSONS — billing management requires at least one recurring billing module + +MOD-PORTAL + +One of MOD-RENTALS, MOD-LESSONS, or MOD-REPAIRS — portal surfaces these to customers + +MOD-MULTI + +None — can be added to any configuration + +MOD-API + +None — can be added to any configuration + + + +# 3. Self-Hosted — Perpetual Licensing + +## 3.1 Core License + +The core license is free. There is no charge for the base POS, inventory, customer management, cash drawer, and basic reporting functionality. The store pays for modules they need on top of core. + + + +## 3.2 Module Pricing + +Module + +License + +Maintenance/yr + +Notes + +MOD-RENTALS + +$600 + +$120 + +Most purchased module + +MOD-LESSONS + +$600 + +$120 + + + +MOD-REPAIRS + +$400 + +$80 + + + +MOD-BATCH + +$400 + +$80 + +Requires MOD-REPAIRS + +MOD-DELIVERY + +$300 + +$60 + +Requires MOD-BATCH + +MOD-ACCOUNTING + +$500 + +$100 + + + +MOD-BILLING + +$400 + +$80 + +Requires MOD-RENTALS or MOD-LESSONS + +MOD-MOBILE + +$600 + +$120 + +iOS app for conventions + +MOD-PORTAL + +$400 + +$80 + +Customer self-service + +MOD-SCHOOL + +$500 + +$100 + +Requires MOD-RENTALS + MOD-BATCH + +MOD-MULTI + +$500/loc + +$100/loc/yr + +Per additional location — see section 3.6 for user/location/terminal pricing + +MOD-API + +$400 + +$80 + +REST API access + +PAY-STRIPE + +$300 + +$60 + +Stripe integration + +PAY-GP + +$300 + +$60 + +Global Payments integration + + + +## 3.3 Common Bundle Examples + +Bundle + +Modules + +License Total + +Maintenance/yr + +Repair Shop + +REPAIRS + PAY + +$700 + +$140/yr + +Standard Store + +RENTALS + LESSONS + REPAIRS + ACCOUNTING + BILLING + PAY + +$2,800 + +$560/yr + +School Dealer + +Standard + BATCH + DELIVERY + SCHOOL + MOBILE + +$4,600 + +$920/yr + +Full Platform + +All modules + +$5,700 + +$1,140/yr + +Full Platform Bundle + +All modules (bundled) + +$4,500 + +$900/yr — 21% saving + + + +## 3.6 User, Location & Terminal Pricing (Self-Hosted) + +In addition to module licenses, self-hosted customers purchase capacity licenses for users, locations, and terminals. The core license includes a starter allocation. Additional capacity is purchased as add-ons. + +### Included with Core License + +- 5 users (staff accounts) +- 1 location +- 2 terminals (desktop app installs) + +### Additional Capacity Pricing + +Capacity | Price | Maintenance/yr | Notes +Additional user (5-pack) | $200 | $40/yr | Staff accounts — customers are always unlimited +Additional location | $500 | $100/yr | Own inventory, transactions, cash drawer +Additional terminal (5-pack) | $250 | $50/yr | Desktop app installs + +### Web UI Access + +Web UI access (admin panel) is included for all licensed users at no additional cost. The web UI provides management, reporting, scheduling, and configuration capabilities. It does not include POS terminal, card reader hardware integration, or cash drawer operations — those require the desktop app (terminal license). + +### Enforcement + +- User creation blocked when at limit — admin shown upgrade prompt with pricing +- Location creation blocked when at limit — same upgrade prompt +- Desktop app shows "terminal limit reached" on first launch if no seats available — links to purchase +- Web UI never blocked — always available for licensed users +- All limits checked at API level — cannot be bypassed by client + + +## 3.4 Maintenance Policy + +- Maintenance is optional — software works forever without it + +- Maintenance includes: all software updates, bug fixes, new features, support access + +- Without active maintenance: software runs on last version at maintenance lapse date + +- Version cap: lapsed maintenance locks to the major version active at lapse + +- Newer major versions require active maintenance to install + +- Maintenance can be renewed at any time at current pricing — no penalty for lapsing + +- Re-activating lapsed maintenance does not back-bill for the lapsed period + +- Security patches distributed free regardless of maintenance status — safety is non-negotiable + + + +## 3.5 Version Cap Behavior + +Example: Store purchases license — maintenance active Platform releases v1.0, v1.5, v2.0, v2.3 Store updates through v2.3 while maintenance active Maintenance lapses — store is on v2.3 Platform releases v3.0 with new features Store experience: v2.3 continues to work — forever v3.0 shows in update checker as available Update blocked: 'Renew maintenance to upgrade to v3.0' [Renew Maintenance] button links to purchase Security patch v2.3.1 released: Applied automatically regardless of maintenance status + + + +# 4. SaaS — Subscription Pricing + +SaaS subscriptions bundle modules into tiers for simplicity. The store always runs the latest version and support is always included. No perpetual ownership — access continues while subscription is active. Each tier includes a base allocation of users, locations, and terminals. + + + +## 4.1 Subscription Tiers + +Plan + +Monthly + +Annual + +Modules Included + +Included Capacity + +Starter + +$99/mo + +$990/yr + +CORE + one PAY module + basic support + +5 users, 1 location, 2 terminals + +Standard + +$249/mo + +$2,490/yr + +CORE + RENTALS + LESSONS + REPAIRS + ACCOUNTING + BILLING + PORTAL + one PAY + +10 users, 1 location, 5 terminals + +Professional + +$349/mo + +$3,490/yr + +All Standard modules + BATCH + DELIVERY + SCHOOL + MOBILE + API + priority support + +25 users, 3 locations, 10 terminals + + + +## 4.2 SaaS Add-Ons + +Add-On + +Price + +Notes + +Additional location + ++$99/mo + +Per additional store — MOD-MULTI included + +Additional users (5-pack) + ++$29/mo + +Staff accounts beyond plan allocation + +Additional terminals (5-pack) + ++$19/mo + +Desktop app installs beyond plan allocation + +Second PAY module + ++$29/mo + +Run two processors simultaneously during migration + +Priority support upgrade + ++$49/mo + +4hr response + emergency POS-down line + + + +## 4.3 Subscription to Perpetual Conversion + +SaaS customers who decide they want ownership can convert to a self-hosted perpetual license. Subscription payments made to date are credited toward the perpetual license price. + + + +Example: Standard SaaS customer converts after 18 months Paid to date: 18 x $249 = $4,482 Standard bundle perpetual: $4,500 Credit applied: $4,482 Balance due: $18 + first year maintenance ($560) Result: Customer owns software outright 18 months of SaaS fees count toward ownership Effectively: renting to own + + + +- Conversion available at any time — no minimum subscription period + +- Credit applied is total subscription fees paid, capped at perpetual license price + +- Customer chooses which modules to own — must match or subset of current SaaS modules + +- Maintenance starts fresh at conversion date + + + +# 5. License File Technical Design + +## 5.1 License File Format + +License files are JSON documents cryptographically signed with the vendor's Ed25519 private key. The software verifies signatures using the embedded public key. Tampering with the JSON payload invalidates the signature and the license is rejected. + + + +{ "license_id": "LIC-2024-00142", "license_type": "perpetual", "company_id": "uuid-here", "company_name": "Springfield Music Co.", "issued_to": "admin@springfieldmusic.com", "issued_at": "2024-09-01T00:00:00Z", "maintenance_expires": "2025-09-01T00:00:00Z", "software_version_cap": "2.x", "modules": [ "CORE", "MOD-RENTALS", "MOD-LESSONS", "MOD-REPAIRS", "MOD-ACCOUNTING", "MOD-BILLING", "PAY-GP" ], "limits": { "users": 15, // staff accounts (customers unlimited) "locations": 1, // physical store locations "terminals": 5 // desktop app installs }, "signature": "base64-ed25519-signature-here"} + +## 5.1.1 License Limits — Revenue Model + +The `limits` object in the license file controls three independent licensing axes. Each is a separate revenue line. + +Limit | What It Controls | Enforcement +`users` | Staff/employee accounts — people who log into the system. A user can access any location within their company (not locked to one location). Customer accounts (the `account` table) are unlimited and never counted. | Block user creation when `count(users where company_id = X) >= limits.users` +`locations` | Physical store locations. Each location has its own inventory, transactions, and cash drawers. Adding a new location requires a location license. | Block location creation when `count(locations where company_id = X) >= limits.locations` +`terminals` | Installed instances of the desktop app (Electron). Each terminal registers with the license server on first launch and consumes a terminal seat. | Block terminal registration when active terminal count >= `limits.terminals` + +### Web UI vs Desktop App (Terminal) + +The web UI (admin panel) is always available to licensed users and does not consume a terminal license. However, the web UI has restricted capabilities compared to the desktop app: + +Capability | Desktop App (Terminal) | Web UI +POS transactions | Yes | No — requires Stripe Terminal hardware +Stripe Terminal card reader | Yes | No — hardware integration requires desktop +Cash drawer integration | Yes | No — hardware +Repair technician work screen | Yes | View only +Inventory receiving / adjustments | Yes | Yes +Customer / account management | Yes | Yes +Reporting and dashboards | Yes | Yes +Settings and admin | Yes | Yes +Lesson scheduling and notes | Yes | Yes + +The web UI is positioned as a management and reporting tool. The desktop app is the full operational client. This creates natural upsell pressure — a store with 3 registers needs 3 terminal licenses, but the owner checking reports from home uses the web UI at no extra terminal cost. + + + +## 5.2 Signature Scheme + +- Algorithm: Ed25519 — fast, small signatures, strong security + +- Private key held by vendor only — never distributed, never in source code + +- Public key embedded in application binary at build time + +- Signature covers entire JSON payload excluding the signature field itself + +- License file delivered as .lic file — plain JSON, human readable + +- Offline verification — no network call required to validate license + + + +// License verification pseudocodefunction verifyLicense(licenseFile) { const { signature, ...payload } = JSON.parse(licenseFile) const isValid = ed25519.verify( EMBEDDED_PUBLIC_KEY, JSON.stringify(payload), signature ) if (!isValid) throw new Error('Invalid license') if (payload.modules.includes('MOD-RENTALS')) enableRentals() // ... enable each licensed module} + + + +## 5.3 SaaS License Delivery + +- SaaS license generated automatically on subscription creation or change + +- Pushed to store instance on login and refreshed daily + +- Module changes take effect within 24 hours — no manual file handling + +- Subscription cancellation: license updated to CORE only after grace period + + + +## 5.4 Self-Hosted License Delivery + +- License file generated by vendor portal when purchase is completed + +- Delivered via email and available for re-download from customer portal + +- Store admin uploads via Settings → License in admin panel + +- Tray app detects new license file and applies without restart + +- License stored in platform database — survives Docker restarts + + + +## 5.5 Expiry and Grace Period + +State + +Behavior + +Maintenance active + +All licensed modules available. Updates and support included. + +Maintenance expiring soon + +Warning banner shown to admin 30 days before expiry. Renewal reminder emails sent. + +Maintenance lapsed — grace (14 days) + +All modules still work. Urgent renewal banner shown. No new updates downloaded. + +Maintenance lapsed — post grace + +All licensed modules continue working. Updates blocked. Support not available until renewal. + +CORE — always + +CORE never disabled under any circumstance. Store can always process sales. + +SaaS subscription cancelled + +14-day grace period — all modules active. After grace: CORE only. Data export available for 90 days. + + + +# 6. Module Enforcement + +## 6.1 Runtime Enforcement + +Module availability is checked at two points — API routes and UI rendering. Both checks use the same license object loaded at application startup. + + + +// API route guard — server siderouter.get('/rentals', requireModule('MOD-RENTALS'), (req, res) => { // handler only runs if MOD-RENTALS licensed})// UI guard — client sidefunction RentalsNav() { if (!license.hasModule('MOD-RENTALS')) { return } return } + + + +## 6.2 Upgrade Prompt + +When a staff member attempts to access an unlicensed module they see a clear, non-alarming upgrade prompt. The goal is to surface value, not block the user with an error. + + + +┌─────────────────────────────────────────────┐│ Rentals Module ││ ││ Track instrument rentals, rent-to-own ││ contracts, and rental fleet inventory. ││ ││ This module is not included in your ││ current license. ││ ││ [Start 30-Day Trial] [Purchase Module] ││ ││ Questions? support@platform.com │└─────────────────────────────────────────────┘ + + + +## 6.3 Trial Licenses + +- Any unlicensed module can be trialed for 30 days at no cost + +- Trial generated automatically — no credit card required + +- One trial per module per store — cannot restart a completed trial + +- Trial license has same format as paid license with trial flag and 30-day expiry + +- Store receives reminder emails at day 20 and day 28 of trial + +- Trial expiry shows purchase prompt — module disabled after grace period + + + +# 7. Professional Services + +Service + +Price + +Notes + +AIM data migration — standard + +$750 + +Customers, inventory, open rentals, lessons, repair history + +AIM data migration — with GP token import + +$1,200 + +Includes validation and import of existing GP payment tokens + +Remote installation (self-hosted) + +$250 + +Vendor installs and configures remotely via screen share + +Onboarding training + +$300 + +3hr remote session covering all licensed modules + +Custom report development + +$150/hr + +Custom reports beyond standard library + +Chart of accounts setup + +$200 + +Configure accounting module to match existing QB setup + + + +# 8. Beta Program + +The first 10 stores receive the platform at no charge for 6 months in exchange for active participation as design partners. Beta stores get lifetime pricing advantages as reference customer compensation. + + + +Beta Term + +Detail + +Duration + +6 months free access to all modules + +Post-beta discount + +20% lifetime discount on all licenses and maintenance — locked in forever + +Store obligations + +Monthly feedback call (30 min), permission to use as case study, referral introductions to other stores + +Target profile + +Stores currently on AIM with pain around cloud access, school batch repairs, or modern hardware requirements + +GP token benefit + +Beta stores on Global Payments get free token migration validation — highest value migration path \ No newline at end of file diff --git a/planning/16_Windows_Installer_PowerShell.docx b/planning/16_Windows_Installer_PowerShell.docx new file mode 100644 index 0000000000000000000000000000000000000000..d14e633c8371c768f13803a78c14ea984c922b10 GIT binary patch literal 18829 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 | Out-Null Write-Host 'Docker is running.' return } catch { } Start-Sleep -Seconds 3 $elapsed += 3 Write-Host " Still waiting... ($elapsed/$timeout sec)" } throw 'Docker did not start. Please restart and re-run.'} + + + +## 4.6 Service Registration (NSSM) + +function Invoke-RegisterService { Write-Host 'Installing service manager (NSSM)...' # Download NSSM if not present $nssm = '$INSTALL_DIR\nssm.exe' if (-not (Test-Path $nssm)) { $zip = '$env:TEMP\nssm.zip' Invoke-WebRequest -Uri $NSSM_URL -OutFile $zip Expand-Archive $zip '$env:TEMP\nssm' -Force Copy-Item '$env:TEMP\nssm\nssm-2.24\win64\nssm.exe' $nssm } # Remove existing service if present & $nssm stop $SERVICE_NAME 2>&1 | Out-Null & $nssm remove $SERVICE_NAME confirm 2>&1 | Out-Null # Docker executable path $docker = 'C:\Program Files\Docker\Docker\resources\bin\docker.exe' # Register service & $nssm install $SERVICE_NAME $docker ` 'compose --project-directory $INSTALL_DIR up' & $nssm set $SERVICE_NAME Start SERVICE_AUTO_START & $nssm set $SERVICE_NAME AppDirectory $INSTALL_DIR & $nssm set $SERVICE_NAME AppStdout '$INSTALL_DIR\logs\service.log' & $nssm set $SERVICE_NAME AppStderr '$INSTALL_DIR\logs\service-error.log' & $nssm set $SERVICE_NAME AppRotateFiles 1 & $nssm set $SERVICE_NAME AppRotateBytes 10485760 # 10MB Start-Service $SERVICE_NAME Write-Host 'Service registered and started.'} + + + +# 5. Uninstall Script + +The same script handles uninstallation via the -Uninstall flag. Clean removal for beta testing and troubleshooting. + + + +function Invoke-Uninstall { Write-Host 'Uninstalling Platform...' # Stop and remove service $nssm = '$INSTALL_DIR\nssm.exe' if (Test-Path $nssm) { & $nssm stop $SERVICE_NAME 2>&1 | Out-Null & $nssm remove $SERVICE_NAME confirm 2>&1 | Out-Null } # Stop and remove containers if (Get-Command docker -ErrorAction SilentlyContinue) { Set-Location $INSTALL_DIR -ErrorAction SilentlyContinue docker compose down -v 2>&1 | Out-Null docker image rm registry.platform.com/platform:latest 2>&1 | Out-Null } # Remove resume registry key if present Remove-ResumeKey # Ask about data $keepData = Read-Host 'Keep your store data? (y/n)' if ($keepData -eq 'n') { Remove-Item -Recurse -Force $INSTALL_DIR -ErrorAction SilentlyContinue Write-Host 'All data removed.' } else { # Remove everything except data and backups Remove-Item '$INSTALL_DIR\*.yml' -ErrorAction SilentlyContinue Remove-Item '$INSTALL_DIR\*.conf' -ErrorAction SilentlyContinue Remove-Item '$INSTALL_DIR\*.env' -ErrorAction SilentlyContinue Remove-Item '$INSTALL_DIR\*.exe' -ErrorAction SilentlyContinue Write-Host 'Platform removed. Data preserved in $INSTALL_DIR\data' } Write-Host 'Uninstall complete.'} + + + +# 6. Error Handling + +All phases run inside a try-catch. Failures show a clear message with context and a support contact. State is preserved so the install can be retried from the failed phase without starting over. + + + +try { Invoke-InstallPhases -StartFrom $currentPhase} catch { Write-Host '' Write-Host '============================================' Write-Host ' Installation failed.' Write-Host '' Write-Host " Error: $($_.Exception.Message)" Write-Host " Phase: $currentPhase" Write-Host '' Write-Host ' Your progress has been saved.' Write-Host ' Re-run this script to retry from' Write-Host ' where it failed.' Write-Host '' Write-Host ' Support: support@platform.com' Write-Host ' Include the error above in your email.' Write-Host '============================================' exit 1} + + + +# 7. Phase 2 — Inno Setup .exe Wrapper + +Once the platform is stable and beta is complete, the PowerShell script is wrapped in an Inno Setup installer. No logic changes to the PowerShell script are required — Inno Setup is purely a packaging and UI layer on top. + + + +## 7.1 What Inno Setup Adds + +- Professional installer UI — welcome screen, license agreement, progress bar + +- Single signed .exe download — no script to right-click and run + +- UAC elevation prompt handled automatically + +- Code signed binary — no SmartScreen warning + +- Windows Add/Remove Programs entry + +- Desktop shortcut creation + +- Built-in restart handling — Inno Setup has native resume-after-reboot support + +- Uninstaller registered in Windows + + + +## 7.2 Inno Setup Wraps the PowerShell Script + +; installer.iss (Inno Setup script)[Setup]AppName=Platform Music StoreAppVersion=1.0DefaultDirName={autopf}\PlatformOutputBaseFilename=PlatformSetupSignTool=signtool[Files]; Bundle all dependenciesSource: install.ps1; DestDir: {tmp}Source: DockerDesktopInstaller.exe; DestDir: {tmp}Source: nssm.exe; DestDir: {app}Source: docker-compose.yml; DestDir: {app}Source: nginx.conf; DestDir: {app}[Run]; Execute PowerShell script silentlyFilename: powershell.exe; Parameters: -ExecutionPolicy Bypass -File '{tmp}\install.ps1' -DockerInstaller '{tmp}\DockerDesktopInstaller.exe' -InstallDir '{app}' StatusMsg: Installing Platform... Flags: runhidden waituntilterminated + + + +## 7.3 Code Signing Requirements + +Certificate Type + +Cost/yr + +Notes + +Standard OV certificate + +~$200-400 + +Removes unknown publisher warning. SmartScreen trust builds over time with installs. + +EV (Extended Validation) + +~$400-700 + +Instant SmartScreen trust — no reputation period. Requires hardware token (YubiKey). Recommended for commercial release. + + + +# 8. Distribution Security + +## 8.1 Source Code Protection + +Python source code is compiled to a native binary using Nuitka before being packaged into the Docker image. The Docker image is distributed via a private registry — customers receive a container with a compiled binary, not readable Python source. + + + +Build pipeline (GitHub Actions): 1. Run tests 2. Nuitka compile: python -m nuitka --standalone --onefile main.py Output: single compiled binary 3. Docker build: FROM ubuntu:24.04 COPY main /app/main # compiled binary only CMD ["/app/main"] # no Python, no .py files 4. Push to AWS ECR: registry.platform.com/platform:1.6.0 registry.platform.com/platform:latest 5. Update version registry API + + + +## 8.2 Registry Access Control + +The private Docker registry requires authentication. License validation returns a time-limited AWS ECR pull token scoped to read-only access on the platform image only. + + + +License validation response:{ valid: true, modules: ['CORE', 'MOD-RENTALS', ...], registry_token: 'eyJ...', // ECR token, 12hr expiry registry_url: 'registry.platform.com', license_json: '{ signed license file... }'}ECR token properties: - Read-only (pull only, cannot push) - Scoped to platform image only - 12 hour expiry - Refreshed automatically by tray app - Even if extracted from config, limited blast radius + + + +## 8.3 License Enforcement in Binary + +The Ed25519 public key is embedded in the compiled binary at build time. License verification is compiled code — there is no license.py file to edit or bypass. The binary verifies the license file signature on every startup and checks module permissions on every licensed API route. + + + +# 9. Post-Install Management + +Phase 1 (PowerShell) customers manage the platform via command line. Phase 2 customers get the tray app. Both approaches documented below. + + + +## 9.1 Phase 1 — Command Line Management + +# All commands run from C:\Platformcd C:\Platform# Check statusdocker compose ps# View logsdocker compose logs --tail=50docker compose logs api --follow# Restartdocker compose restart# Stopdocker compose down# Startdocker compose up -d# Updatedocker compose pulldocker compose up -d# Backup nowdocker compose exec postgres pg_dump -U platform platform > backups\backup-$(Get-Date -Format 'yyyy-MM-dd').sql + + + +## 9.2 Phase 2 — Tray App + +The system tray app (built in Go for minimal footprint) provides a point-and-click interface for all common management tasks. Store owner never needs a terminal. + + + +Right-click tray icon: ● Platform Manager v1.6.0 ────────────────────────── Open Platform → browser to localhost ────────────────────────── Status ✓ API Running ✓ Database Running ✓ Cache Running ────────────────────────── 🔵 Update Available (1.7.0) View Changelog Install Update ────────────────────────── Backup Now View Last Backup: Today 3:00am ✓ ────────────────────────── Restart Services View Logs ────────────────────────── Stop Platform \ No newline at end of file diff --git a/planning/17_Backend_Technical_Architecture.docx b/planning/17_Backend_Technical_Architecture.docx new file mode 100644 index 0000000000000000000000000000000000000000..88f7c6b28abecc524ed886445c370b7e2e5c9e47 GIT binary patch literal 19502 zcmc$`bChh)vM$=TZQHh2+qP}nwr$(CZQEF_)mUxg^>6RD&%5W`cgJ}Dy&5BD)u zYG%Y2nGu=sO?fF`5Ga6uUZnA!+W-3Se;+`9-rejRjp^n8k0Q|j5z%urv3B||L5P3- z)@Y`j$=ib0k)$Eam=UxZNzG}STCOUP&a=b4k zb%k)tp=}acx(f3PVjR;K1T)s^J1-kV4MNz2J!-WwnLy=xb~AQnh128$oey zAEX>ooZ(xE2stgmsf*0nB{;)65)l1Q@I%$V5w4?50mBb>mRPcraRw_%0$WeIQV6Cc z;SLI{vEs2>@(5&xBr*wyNA&i*0so`if9nhb2_X1@1^~c*1qcA~KRWwgwRPp=`t zbgDK$_WZyh_xOjywo;eh?K5@R=Ww`X+x3}m>j*j_J~)#^9ih3#xy$T(6TaSz|KO8^ z8`vM^F>%<#Dj70)HBlVuw;cn1#aviD(6j1Jo zzjAf%3oYq%Y-Em!f|86%lx+8_a-EfG>n6KlYfiVv6QqkX1h;y2DluE!>XtY9*%xZ@ zA^e!^;eYQ{>Ya6MuYLX=R~p-Gs1gC1%WpKKy622`vw@YBtrD%(13%=FInkNl^XTK_ zSo{FnJ%QalzBPI-VsViBdP95f;eLQ%Q}{lli>r5^`y-)9*Yp~*7r_F1_=Ml11H3f_ z`#b6RBD+j8<+Ypj)wd?cSj|O?<7BUpc5gz)eB_;X+J5rh6GXJl&1(7{x3z64rc@i- zf!nU=do_mVkMEXG_vZW)Hwy?r+k2G^ulGf%YuAf1HSA78S{hbhnWn{gtaQ)zB9?vE z-_^w@OR2qXEh%_4Ezb{I+M0S~h{P&4bII8;3_1K!yPZMy9pw=s4-WLu&mHk?2X|$o zs|Nh)9rIZ5tpqpk+{&DtPTUReTsNJ2GjxCckY{>D77!Nfdi}D z%bh5FA#|)b*9+He{O{L@N$CI?v;>fd-a1mb`h{|HZ^Ukf_mJN)zm`6$%j??MaXb3L zLVV(s3!NWPKFQ(n|6aFGMW(GRh;6MT3>n_4xB2n~DYuLR>eq@8KD4wyKQA@LBVuFq z&&Ya=k`G!&nB@Z=G)Kc?tVIKVi7Gy|@spmTg}3;8%oGLfm>BYE|EdmiBVGlJs})9T zf<}~K#=H%_x-h4T374)IF_WnpPdfs;oR`x7uB05KlIhOn{9}7PhEFVOYwlwHe z&#-BueoO=SVWo{S_Ab}ui{=^Tkw>oorO;&k_s8CM+(=W~ns^6Ju&&poIg_FZ+L4kkg{g;#S&hwZ zG}8A8w`cN)BK+Y?D)GJl=f~BM4!!n%Ah5iP0N1)5Rh>FmOuewWsqjFvTU?sx!P?>R zE-(EoG9#Ov)2BElFo}1Ro;hbm?+PsmOtiZ9_Y?o?>oxy5U1Cj__}?1h+9NGs%Ep*7tm+?yb0BJ|6|)_A$%mL`DlJxwboq3s0a*vtSmg&iM4)%xBs%YH zv*l|2DmuT1dsR&%cQZ2z-O4f!#f6a`BUpvXxy^RineABtfPlflzN9c;T&QwR_ibq- zf_QwHx$$|qd_RiO*F9<^zplMEl?N1hx-%kM@B#^3{*IRF#)GhZ)?2?78ceC2Af$WM zs8k`$aMiR#uS%k;T;N!@ghFc816j8W z-$<7?imz(w3D$-IHc! z7TBzdpaodFSy^2n(hDnPHz`me4;il-dML^okGdOm1R0o_%qM5uRbxTXq&Q@UW2X}Z zlE7oViN+-`IVpa0M2eiY5sU<(>@mRkz$w0H(o~;pN0v7izKUj6P{9uQOPmj)VnRH1 z8>wjqR6!55P#(MkLfx`K*tu2oALl1b+ngkEuLM z2Sm?yoQT#J9WaCS5DHADNia3Zrl)`ELeYvW7!2X(wZxoYZVMZspnrK235cDV`eIVt z23ialZ3F7#a@;U9M8o*P6Y)}9W$|cgho>i^`b z_>G7v-YT~TMUzuRnN<%vmB)+mWD1SB3hd=Ve>Gv2zvn+v`-IG5x9tR)2n`d#F>yzS z$>70%*Wwt>;YJCXg5&Mb;NfoC-p1OOMN3^MEtrHN*1?Q3nGoZD_mzNBEQMIqKLA%r z#Ig1_s;$d!kDkME%NkP|2m!$I(CxgP8pc;=j-SmmZinEk3V*s2RyYR@w{c%8n@C1! zHxw)!zhCXXoT0qv6CUa#N$!NB$O*7h&L94sjv!4{HI}$i%}FpP?wdo!R!Wo2F_AWi@0$(1`9)gVPKHwz64>nHtu!+ zHYa!_LQ(dkVQPm6EozIscT64!av=*nLL#VDiTK(CbV9gbpJnj%OGN+dTDkg^ zLofb<`_dwRRI}&NU7t{ew24^2ezMR{zU&3v!>d%3(k4M8>V*@H?)Mbqt=28AIy07oMo_g${@ z&xQB6G?5IKIq+-iPC?!-S$F-Md+u}4B?HD!cbCx zKUi5i454uT23<$>2JJR%V*mhXPA^&-xO@q`@AGg<^l~ID3_P%w%}ilp}V8TsYO?`aR++oW}4(9?E2 zeg359FEwxVw3}X`$Xv|OpDtaa3jmWQ1^Cn^H!ts@!*vP5>%)c^Bv+tcN=FRLz8O$8r3EITP9o47z>kiTBxfJ3QDVsR>9?gn*eItxl#==NGuuvs$%ATw#2XA0)c zSx9tku*It#c)M2Xo8u@e<8$P6ZNXkjF75oGbX9w_KN$JSc^;F+nIfT^pg&Ubirj^ z2@fq4j_p{y=m)l13Oq&CEIjM3ltjr{e+A7ex~TtH=Hs%|^< zb5i9I&A!ku8dwzZTPgs+C2_sN1V&(zlmxpg#P%Lp6yWu7cvxyWno24%h>8JigRB{a z%tHXSd#S7%!838yI#(v?m=9)H5(ENnWJ%PFS5mgzo^5J}>emJo$QIU=Ho^o`#u~pZ zUBPtt-kS`sejwqQs$VNfFw_xhl*8X?>Nx@P({Y}A%9LI2)nXYKlHK#9@aauC(#k#Y zhdYw=UY?t)+seZ+_fWYMYug215C1CXnbYZ{lR$SF`O>G~$g8!VQR7h?aidqc zj4+on!i*6pzAl2`idOFJ#pt}ok4D;@neZj6n3}e3v#KyR(#KN z$SBu%kjoMNYyMu*B!nMQBtucqaMnaClHHPI>r=z@sE}Vu`rn&otUAUbhri^whVz1p zSB-g4X`P#4o^a|Z1a`%T8&ydBihx&1CNSMA2>ReJmMA9D>mj+ zGDpJ4YoUpXi4nS~nCY6#39n@U{P47{yr7xg%0~PX?q0l}Cq0I`4r!nz336VYRkh?u z1mEWG4z-1WU<{rAC;`8o0vi_1F`oFMq*S1?&08$LRWrVQ$f`$1OGMG`bUr)wHSkJe zVBRBpmN9}h_A&a|8%(kx8b6u=VUC_25NK4E{ zc^jh2@+7SHL|Pn_Lu?IAJUD9LZAqAt$g@%0%Yoy*c@68$uI^PCC8eR)T&p%W-fab5x?ye@aSopzf8{KKW|D?;W>{TNlj?(DM=hjeR#cS z$&i9W=+twQks}nrg@I0Bn_&@|h|UQH95rg2C=3jv)AYgV9WLSfv}sKI7NAVQ9juzA zZY85YE*AHdCG3$*$_BZc#YZWsh&qicWi;SlW#$!dT z(ckt`oAZasKsyvW3sQwn%+_jB@3_A8l7+;=A*J&U6f-3sS|mJnuK8&C|M|Rm7#40J z&Lm<=14#cle54UInzBM#u2+g92hO7@gFDO@3g<>w*+lg_| zp1omNu9pRpuX+fAW8jjg8v*l4><@tMEh;t$JBJzOpqxwtYir7=KmL+jHcwu+KNG*b z((WPqi3}PAQFK*If+)JF&bd{*VOx@^hI#OkMp7x2>RRJLB)}ml*lxp-a89UmUC* zju4}eB65GV`NY5(a)~oQ%t&OqqV`BfGp7M3+#I)E<+2{NJYx7tLvx+5&!qLkUony~ zx}hxkt7EYRb4V^;0&&$F>cE&}bv zn~mYL>Ma39x39tIPB5J$l7ZyKC1Fr|rG-moU~IE7sbD#B=>9xY_|{S#mpG{`W@B%g zAGCWeqrsGKe^jw_uym`p<}G(yGL)YOdb3@3&+I-OU9xeF4WQBQ#K>RGa(egO#cr9? znTHD_GzoIoW{E3bDhWEyJT5W7xCge(uTqFWrSU-=PK|m#P#KQfGQ47L-mkT7SIS6- z+X|~nB??3GfNcxSQe6SxOVB2a&V}s%{~`-$k=h{)SGTJZ%95_ z&nZ)#tQzer1vm!!`pzWZ*01%=tIOgb43!{nDalY}N-lhUL(N#AEnMSQ8N?rBp$iebKj1d{`~1_GVT=^xzaugNhy znya?tV>y6U`%Z$=;xZO3GX!drj7>LF=(8DdiLp}{>dogiN^D4@T>}sB?P{$*At8-H zI7=RUkOJD`;;P8mQ#IRIh3#59WP6avLujh}STY7u zX0o|I(JBPcV!@EOM8DsZ)^xbW0uVJ8kQ28W`k<{LDa{5(XqNm6q8Onie~SyMV=|!y z#ecgdHVIVsl;({xsp{sUY58VJ@UUg9bm`hK6*^hUdJdYR&v1WM7mMQx9nKsygiD^1 z=kZ~&Ty`#dqR~;k;K1_C1`0aJ9XX*Ib2tRIYZXWv zqZ>6Zk;%n`M?Z?b*@O3*3hYRbe2wYY^9i1D%X9vAw%aLpglflAKys$ODlAQ=lI2Vx zr@B0m^1fRd$&lp6)F21~7HL|}lSp2u_nP&u@tt@J1~h_80!3?UB|tn^Uu(35gNM!M zPZ;Gqy_@VkW!8sct2YK5WsBmNJ%57(Q#o=`T6UlU2UdBvw?QG(qQB9eVaHP)gZV~W zbbTrLy`kCSZq69^$Vl2GH4}r6=}T%{br5}bDnT`Np=-~V7^1x4|L8@lD$E0TXx+qL z5eb|J_JB0~Sh4s|tW+7oFb7_xHjfsMx*MTtVhavNmrjc~lv_-Js$F<7RfvO%=sg(I zrRrrlum4Gk5>XIS*)yA_Pa;MDjTZ9z52{{txpz)I$rxyQ>cBLeQvvaO>Z-%QA*vo3 zI?o$XIzaRZ6D_iQ)ZrR~&`~9QIHoQ=S})9p6q<0{;dFX9WSpstoaHaUaydoe>qUy; zv~A`23k+&_KVOj&3a?Q2THEmRNfW%j0E~O;aO9vw=c1tEEfcwZ;T%Ti%MmtkCa$m0 zReQi+ZqXXIj2RA$#~t>F#!pqS=Ow?u`w$|PZ{7(vaz9H)T}UVW8h&F$3JV8AMb{AV zPwXdqp!&MG0{XFA8@TV$!f>nfP^hXwacgK%TGKKPvW3R=#u$`1>#V7H5tBpyfpVd4 zxe(tl%MTu<3d*2AqDK`iv1bxD zA?%f$cDrF599-z^#6vHmR3Jvfcu`j(uDi({UOChL$xwiRGf`mA2pWu97tIw7N~y>= z9K!~aUlG!kE3K^(IJ3&k87YmnG4N!3{^oAZlBUv<-3gh~c|vd+R-;SmL`3H<4y`X{ zZ~Ru!9EyiS;#TV(7sl2pqRDjREl_FP72I_6hyuHu_vLNcL)%!%;cPP*GIyZP)H-5{ zvLedfs=El^pOq&8`blKQ;jnLMn@6jZuCZ>yE7(^Etc9F^B_Pd`hI9v{q^bjk@Wf`_ z$VQ7tI9M~M##SrR73`sblRlA z8Lm;~MzmL1LE6TbWknB9F6j-MS1zMc<^~U_LBL2VVInyl|C6*(~(ec8gpc-I3xLelU92F4Ap!bn_^iL~SVMl$bTd9}`yis;d zF3XJm2@qnS#3IzOM3iK=-j&{Ux<4WQcZf0lqX8ymhYXsIi?u`|>z$7?Ep|VpqQVR0 zB$s{Y3YjdImNdQOUZlKth-}&mKHdk<3&TfpP7kAmbgHGZtQc z*iQOl@3M;%XUP|L#wns!&{T01L;v;U)AmRBQxN_LEf_Ok=AmK3mD3~)=LcSv87@Zl zV5EozL8iYFTXFMRAE8o5t1O}RG10={)^)&V(>3nPm7Q7hJWS+*JuGc)w#Si~b7^o$ zU0z1httUv~S65f8lA)@SR2?n2t@0~1cs+C@dv`Y_V7M?-9RitioXh%1NStu-M+=hc zuHGjgj>JxkL($j;xOrbyN_B-gs^~hC>B&Hs?_4e$vpgTBoVbDJMhbtGFzdf>C{4F6 z^}cRy3f7L8n9|Z?ZsxuT#Ql{qVk`MNqr4dzIn8`AmF`M7hLd@yWNt5)kOLibD-bn> zCC6ebs%k(u0J~;MMZpxk-^eK??#D7$ka$p1u&THUp)(bq5wX;NmN-m{sXl9gzx73@CWOx{Rp2?-e2Cc1 z%oCt#=I_9#92!*3P*3nK8u||Upg2B;t~4v6nK?7C*vromj2!sRZN>HVzpn+&@d+7j z4xXq~@NMZ8A5rb`UDgFN8l09>me|!C+KFxWc^*Vjeq+^dxdBIsn+$MZ57BPGe`I!H zG3|(QVqQ@UOebTL*@`G*DX?737~@1i4)J&p&nTyWWkxAyP7RZR=`$pq;64dk)R!JG zrEEPcnnFFxfX+d63x<*mL$PAy5HzNbAU&@NWU&#!m{PE%Mo?&Thlr0}RRo3}2qlnD znpCHb?xEw&QMf~)Cq}dsG^LYM1)Z3TK2|p+5T7)`miR26Hx+rT^3++B4`h#fJ6SZnLAg>@UEM8=jDxccgHc8~+8>ITyC#)4GAMvhX*D%Tpg!+LdF? zFNo{me1&ig7pNu>)yRJqqHivj+Nvus;sJ+jZPrf2rrV-=uO>jyAeObA$4-5+sA3x8 z%1I28oyr7>{Z#FV{|vx5U)UWw4U0wvtLp8TfJG3e#3onD663QSn9YWqiP~5$EY;ZJx974I|8)+YdmRQoX>jqm4_o$Rw?2|BXy|4_ zr2TuK^sBIPZZOa_en3ZPX!dkrG0CxPe$a0|ZqU{g0*(GJXXfJR^@9n2xZy{qZ**3j zRu8P9;6~rv0$~1!W@oCw&RpFU+`y3UU9_(u%RBCY3HtT>yxm;4&w!)AS$#jpufyaD zs{|JV4)`}~zq4VSlFQq^ZjSGSeBbAsQHLtidbcd!vtI=z?hMLQ^H0n?5&}yYR*#DQseplNmBh^s$j?NF zR<#UsF4E+wT{0?3aMoPn*J=Y6uy)Z}Q~Fd=rZpW_p>U>ExG4;5GMwwX=MNB=%^WQ^ z+szA!)c5IGpy!%76(YCMnKY5^+(?__4bdE;Xl(r#$t{oRE_EH(WRAdZ-ET ze)!j{b=(GsHg__jD*qfSJ6u|yDJK1Pu^eSqn5W`t;Kl6jcEJF93S8%Go0l4yy4 z6t{P!=oh?DI)Y7y?N@d{f7M1etgiTD#;}T-9~7so1_mewp~|G>ekMyyYM(QoBAQ4_ z+Cu~sByYhlmzkIiNh&T%K72PlQ?G;TW3LOR6r0~@2iWYdb`!g!RG-J>k(W?FDQ=SB zlFVE2cevPQ!(<=m-ZU@-KN+ryc4{ru(GUvVg*C_$hI3-q1F^81FqSLL6j@0OtB4f8 zS}D9mT30)hK)uM9HbWG~bPo3nnYuzYNkXD=bCpp8UGe;J@#M8UekmsfyjCaQT&*0l zU{)IQ8Ks8UiaE}%_r9q}gTHPn^JR@GQE98z5-(z26*F<2GHk2f=mml;)|4#OX8X9!km@{7YQDul#@Uy|@$N`H4AtxnO1YBK z*#<<4>%0ftED26)(5+kROU@XstV^?%y+r9F|1DsvcgNAYOmwQKcam-uPcj{bW*TS` z-YA7JjLDep9N%7{S1atX;XKiwXA&D=<{M+o8L@2^>&-E@%hOM=6F6ee&mi}4g03fF zMJ%&4$5wlv6)O{%20?HW5?*R;t{ zVbSmI=Jr}sEHXm2BR9z)!!(6>R1W*pbD#ezq>HR_LF4CgZ;!WL!eQ)ru=@m zH}y0E>O2s+#*JL!F?udlQI}(2Ip^MHf49E}d$Nb4&D1_@;?hT>9ce!x1Q`0egyus# zqs0rK=h@YYmgo%0&h)rQY%DJC0f@S8r{j@NQizRmjob*@b8v@aX)2~a13#4+TY)f+ zt4$CR;-f?3BLZ8!$_1{gzUgPZbCBy)O<)F1YDJu^aKv_)q8uO~fjvDzPbZ(z93X&^ zpU?;|`NHxm*n*$onA~{sv{jV43*$`_Rneq|s!npNymhXcNw>NdOWA)PbX~s`Y$b(v zfd`dcvQBFXCQ=~2BQKseZ{N4sK^+1jx+5TZ!}3zka;vAHhKdR=GqC|Wb&RDr-JF~P zD4>8nyyyzOIDno#ihfRnCW=k9 zFC0A;oC%v}eo*?|a}$>D^1&^L4q@G? zY2B%BS<*>boawjsx}g|VCsL-!O|rJ>4Wv>^u=qg#OdcpZ{+&6M`Wh$5x_DJpyb*!@ zd8tE1OW!)XAm)#%G@?0&4WSqg#3)3yreEv2F>DyK^w|?CSf)VH1Bq^CvvEEE@yGD@ z**Zfz_`}y{45xoKHbsM?KQnWn@p{M>sVD#ge({5beGxUO@1X1`;ko2ZRCn7jW}HY4 zF?VLa(WUm2qeFNK=%2uvo+xFV5|sn~tIJp0bKAC8`9L7o+cek7^r5tDESe|jBAR7P zN08j&k_O34OD%5#^T2`p`azs;=!}NrO*|elJXZR%1h)J+OgF6W8kzYpKUXtc^Uy#G z&^D*#^_TI;HdsQ=2_uz$_IF`VcbSRFRW61bH0kfp%^b}!&4nQT#V`KnWF~$pYhNFS z&uFx`6Ag$i11%FlZ+M0RObL}KLh?pSVgf~{rn;YiJXj$7&%b;2u`K=EC})?3wnsXI z&xk-Kk=Iio9>R-L)60FB9$jTk;3@cf7%kheM)XRdT;63EB!B%iSkpCTUI|bh#Jx`Y zaW%s?IN1&j8SOTcRce_XTm=-D!2W_N zA9zETCJg6)QYpu|(kmk0zlUf)U-G;ecs%^8l7dM3IY5a$TS-o()Si%P`aXb}J&KuL zlJ||E8ng8Ir;l7BCDMkeQ`#g<$h2Me_@0#`^~;$(nI$|&$O)m900X;w*1Ek2BNxm{ zAfqrsPf84AqmSqYWq$iput!pA`12;8CSyZNYLpngu}A~9#TkujtL<<}zWWDqvqR{a za~N~KO7r?NutziGf;#Zs9f#ZGXx9HFFkFDU$HTLuPiCo6`0u)G1uxSt#D7i z^&k8kWBDeahLT8%UttGbm(=g2cOZDPBS{RtU;V!5xhI%LLI;!o)>MJ@5Y+zxH(jBm z2f2V>->9F1!rosqxZZ#AcNwnHXUD3hDd4tS2EVaMUaog?GwLRN;1$BdF35VgQ|rEd zUEOwWc=a|nJ!`fv??dMa&3ath-XC)7AOGZ~N$os3c(QbBH?q5o9m92XU*zR6H1jn$ ziFx@ez8g2`YjtD02m4paqL*IGn!gd0`~sKEl*+orQY&~8wQ?Cql*itr#c-v;gj${E zPWaWlubZZ{UFz`#{};!Wy!EHuC+gXsL`a-g%i7^B%GU3xcwGG4I-|S3)Es!jPf^I8 ze;1iaI$x`{J@f0tXL7&TTik zv^bm3nn;oJ)Un3VIcR_1QjPtTp8ak|*O(!vOxw^e5M#qx6nE1Lif-^78?_KqLeycL zkE!wj1>Is28Q#2-#w}aq$q)AE8p@rRuCF;F>`fYcLU>a`@xR)vB~BP@T{55pIbhWY z;dcs;LoigGVRxx8VS&sJ3$eLn+1(i!mgL&mAzbO}%Bd_&&Kg2=;T6QsRSJZnrzLA- z@LU}z#$*U8#XWnpeFQBv-aRQQ@B6%&8nj^!$B<0#JFm~X!?-BW4oZk5RiZ+w|7=3$ z5X}U7r*-b{Vw1eC@>fG+?Rz?4&?+iTv_*>B4dqhPk&MUOPd-WU`>m(aE(`K+ zhBqV0dYd)$Ofz|e3&I#j@Nv0t99j+CRgk3Jj?L`j36Bj!LnVozT%wVYW_+8K3w`FD zH4zf4#*L^cl;}PAGiIV=z)DiXg@gEKQ+^>?^rT0r|Xj)%-lK{L5 zz&G|klMB6fx=||JVr>ll!xSkf!NEVPPR|<#g-iy=8O{SJ&iyjQ1_>eTtYq|HJ9Ic1 zsl-(Z>L*IH088p1h2MQK<_JV?ZDuT^Sbg8(??AIi?= z@iTwJOTH>*je4N2#8svtpV2=n9`2-PzWwtJ14p`X$wRmSomjHgh-X8(b#kz0F&HgA z$CaS@Tg(2cQDirj=!&-1w08fP*&&jAR0KhhRSXY_SGano_kc`OjUOa4Y~e&_E{Sk= z+ZKF%w_$gm(%bi8-ADB%KO^8t5YK`x#;eoYagwzO8-0Hh*R5j5l;QhNE9rC}x1Hmo z4V8QJ*QuRn-0g)r<FL2OfMe3>`Ewcj|pwW_)j^M>1@i6l{ zps!*f_t)QV{Qq7L&1qw|7XSTJovgFKh~mN0rNK{@j$X>mu(Sb?{KJu07|_GB&`-_o`Ou>v9Ed|dwq>0XR zxN{lZI!g%^Apefp>(2&@zom{Gz>N6gFM%MQ!%#*A#7nWY820Bb{f1cN00fe=4;~qG zwAq?7Sf`_4fngL@Ln&n>B6{x~ktacfX8d6{gn0<(o>T)E3qZO}7r^HbH*pWJUcv+>k{QWHF*i2;Um{u1Q<9ahd3Z ztj0u~yfZdsCwS@EYWZ1ffniyK;pq|um(kxp#cBm-EAkJT`Dd((51Yl|O`v!V7qBwV z#fvt@i@XAY*O=XV7~OvUitpw({2)cfnvgnKTG3vO!;^nbiw1TL=&qsVI8BdPRj+Lt z>I|*nfM-c?P{M7X=zM4fe|J>6A5?Jb=#`3u|Jl^DmA>U2N}d2$>3x1s_8Nx)KRbm< zAdgs4hj3l{8~rend?NZtqPg3(g{(X0EL}mMoDYsdM7Ec;QpLN>4Wnvp5yd6O*gGKT zKb|T*Zq|Sy>aLkRszeDK219O`w>!SUKS^arMwjfS)liW+esxh#obmK|AAhE%0F?jc zMU0kP=KmjHulT&*yebXRV{T#loxe*N@Io{!!oR4a(4E3 zIr(fg@``!MNhYwZ3znAsBWk9!W9>;(_I;lmqnO6A#m4#T$h6{g4r`ERS&ZOSli30p zBaYuMn6@>F(EAX>al#U>^Ry9rFV>ufH5U8)iVg)BHzk-Z1(;{(ksIsBraA9Czr#~M zfd6>w|IQwP{PETw;)1-Ro&Ent4566L;sgW$2m%857s=tj#Ek45P5yD~ltdk?K?ao2 zZ^;RttYTcEf|`2RIx?72>q|NkF<2`;ai*$oUz?RpMmyJxZaKVNr%gwPWf?~R{A5o3W1({F_WN ztH^HY|D)p~SO5T|{|uhCE;fcHjuy6N|0sGzZNheo0pSxh;5j&vh&vR4)Ev9megW3} z4X{~wOSnCtxcF_=3%;jituSwu{#?HGH>rT-6vTe6`Rv`vOgwXQt!*y^p7OGKsc~rG zJp!omNu|DHYQfyIn;9{Rx|p$Gdm_s(Xv>9}N0ppOMd@XG+gXc4O<>#M zKMl`GUJ_rI!WG)bBHY7gjqsti7`4a(89Ly$t5~o{V!*-xhr0kgUAGUq*(#TjzXs`osTRl z+%L-C#TV2IB9gjY($-M)>6t?bpdR$wGcs1YpSo;cAUw5pAL{q(-|or)fZIUyXe;0^ zZn4~IrkkIG58cW-4@yjMy5Ju2elAvgeSGXr?ES7uUOJ!D_G9riXavYXH#=i`l3wIY zXfhSogZX6_r8e3=ut|!~3;${tmKc{<4F3M2vHtzyLh;Ul^`!6TK!i??7votkC;R&Z zU<2bRo5?{cZ72-$%DryxnQ`;qADz9ag3?)2{BubC$u zewrMcNzzqcR8C}UR1OZnF|fCopvsvjY-cpPvlV^!@#d@MU(UF|o}fAIRh4RAPmEjj9rk?zqoBhG{KkT!A%lw-|mN+K~%!m-CM>^fZy`?H#Y(cMhCR5BOAa3dw zG=op3RAXmH5vlrT^H*HP$H^4?FSM#(>lJE;hf2!~Hk$G%HjaExSh;N%6S&o>QRJJL z6%kA?+R8Q&B0~g;e-Wc_Dbg2t))4L(`ZhQ|#qBxbKQcEn@u>?lpxL6Pi()%60N+WrciQ6 zD*sg*HgOqzURSu)sFU>6!zPW0Fg$-qM-w;*K+N`pFn)P+~y_$#zzshl_Blv=`PU3LcX` zPnJ(U2!`v9-JX!WHs1zo<&G@W+y~So_aF5Cd$d;f*dj{&G2!o@_Kzw5pIOnx$=S~4 zA5-Sc3PKGsAcP!LC2wpgqZ5@_upe%UxZp2ZV4Ir?kni>efazi=WIK|;GkcBB4`wI_cE0(7qaTl>Go=A5@ z)j7r>ca7C%LR*6aeQC2ZCQIoiYKNuTWt7?0o=dAziPBQBYyR3ME{qJ2k{K(l% z`nmOg92DVy?Sg^5{eQpz;7glI=!C^)Y=^7DPWXvtSQchNWGOE%;y`fw*O!B%mQOUa z1|dVa^5cfe5`gC4JhGa6p<4$;pk_g{G{O1Da5V3=J>-$`jo@gY0;`U*oNST}IfNLs zlB0^Ek^{e)mWs`BdP1l-)BvXcD6g4;YJoZL!`*^U)~#2+>MbXHejJ_Fj~!NA|Bnu2 zM@%Hl{VP9h$D`0ax<@ zU0yey@Nk)5N++c)qp#9G!aDG&^Hg8Yq|ip!yJP!@2@1daV_gCGVG502d-}$kR)s^VR+Q)$Mz4%eT#f>R_KugMp@(D-jqp z=EJS6uXBR^cR8$pQH7Nl(FMV)PEGhRf%#mSYKow4k?E09t6%a3@-gO>B+-3#2p2Tee382%&l-hkOIEohy@(OHp{LJyn@I;Q1J1 z<^2TxPbXJy-Vn2`D#>i@J&a`;c3&Bc)plQ&Db0f(kfj70Or#MEIFc5wps)Au2DeGu z)~h;n(iQ{|dHFs6pOJacHr+g(1X;GX)DIODhRQJmM7I-bd7_KmCD3Ui*egytgtu}2 z2nY;nkv%~^Oa%2u=T2ds1&3#w(?Kkadp`z^3z{kJ2EYZA$ zr~!0i#e}!rsd~r&ESI=dA? z4WO&5nzIjw#kF)|$~^9bwK4hy#me%|!SaiVaVt}!J0SBkjvF|(heA1+R;vDw#J5&b z5jy$#U45JReuzmds69xVu|~WI+xo`A{qMH{tx-k*ta>Vl?rjUZYU8}SKU8$NS$srR zKnR^0OJ{t(rLSz4D)>Dnl?2a4>1GO$VSkI#KghHY<;t!gq6&`yaZ#4e9uz8Zq~l95 z@zSrhnwS3OJ%+u>DeLFH&+z}Barr-p^#7~X|Nn-iOy1;V!4E8*ejXg~U$AsCad!Sm zHvY9*QyvJv5h(S>@M=Yt%{t5{bNb@g=MO_nPb z{$eBs7Vty3R=#>}!UwC?OH;tVgrZ&kFEy8gNn?SOg#lWF7cdF=sYv=>MPGMUpuBN;-WAT4b zcMCpfkL5rUOEuV%dDA0T^a~BUn3s~(UFDVlvgFXxqFW(Si}hfW^<*mly6;eKTfGDow(Yi8NrxK!5D$u)9e6v{M zTSGg847I^IkF3VXB8yCY- zhqU7a4h#*Q)3i@-^q$UXJ^sPqjK;62@utBrTW-G^9IVY3 zy5#0)tF5(+%GW5G|7!}z=Zh7J`0iML_2-edm$aOB?c20I|CWZ&+OM*9@vCGGXntJ( zxBJSwVTQ~z&U_ck~2pTQE%hW>iA^Cyt2K(wL;Ipznts7ZbscTE|@AK$MZPdY5=`f_W@3*V#pZ9nR^ z@10`$%R^_W?bHQY_m13{<}@LT$0I5!y9T(asiEhkWFJ$C#oXJiP8q3N*k9-D>9je` z^)KO&HrKVSq0eNmO7`^E#<4t})G<%$_N4`}aToO@N~8lM7hN&jv77zUuZfX+Z5Jy7(zVDanb(PB^JB_4{5HCAfk}Kf zDa{_FARUNEXJis#zT@K+medtpzk*00AH^%0P_N!VB<*X@qpg&`+;{ z>I70gs2YL&G}NVuh?CI=hG4oF7#70yfKxEKM)V;RgvJf#SO-+l4M6X>BMhjvL<-|%csCy10Q7D% z!T>ufBm3^l+O{Tw%t F Postgres ETL /codegen OpenAPI client generation + + + +# 3. Shared Package + +The shared package is the most important piece of the monorepo. It contains everything that is used by more than one application. No business logic lives in individual apps — it lives here. + + + +/packages/shared/src /types account.ts Account, Member, PaymentMethod rental.ts Rental, RentalType, RentalStatus lesson.ts Enrollment, LessonSession, LessonPlan repair.ts RepairTicket, RepairBatch, RepairPart inventory.ts Product, InventoryUnit, RepairPart payment.ts Transaction, PaymentMethod, Subscription accounting.ts JournalEntry, AccountCode license.ts License, LicenseModules index.ts re-exports everything /schemas Zod schemas (frontend + backend validation) account.schema.ts rental.schema.ts lesson.schema.ts repair.schema.ts inventory.schema.ts payment.schema.ts /business-logic rto.ts rent-to-own equity calculations pricing.ts discount engine, min price enforcement billing.ts proration calculations, billing groups license.ts license verification, module checks accounting.ts journal entry generation rules /constants modules.ts MODULE_IDS, feature flags tax.ts tax rate helpers /utils currency.ts formatting, rounding helpers dates.ts billing date utilities + + + +## 3.1 Zod Schema Example — Shared Validation + +// packages/shared/src/schemas/rental.schema.tsimport { z } from 'zod'export const RentalCreateSchema = z.object({ account_id: z.string().uuid(), member_id: z.string().uuid(), inventory_unit_id: z.string().uuid(), rental_type: z.enum([ 'month_to_month', 'rent_to_own', 'short_term', 'lease_purchase' ]), monthly_rate: z.number().positive().max(9999), deposit_amount: z.number().min(0).optional(), billing_anchor_day: z.number().int().min(1).max(28), billing_group: z.string().max(50).optional(), rto_purchase_price: z.number().positive().optional(), rto_equity_percent: z.number().min(0).max(100).optional(),})export type RentalCreate = z.infer// Used in backend: request body validation// Used in desktop: rental intake form validation// Used in mobile: same form, same rules// One definition, zero drift + + + +## 3.2 Business Logic Example — RTO Calculation + +// packages/shared/src/business-logic/rto.tsexport function calculateRTOEquity(params: { monthlyRate: number equityPercent: number paymentsReceived: number}): { equityAccumulated: number equityPerPayment: number} { const equityPerPayment = (params.monthlyRate * params.equityPercent) / 100 const equityAccumulated = equityPerPayment * params.paymentsReceived return { equityAccumulated, equityPerPayment }}export function calculateBuyoutAmount(params: { purchasePrice: number equityAccumulated: number}): number { return Math.max(0, params.purchasePrice - params.equityAccumulated )}// Same calculation runs on API, desktop, and portal// Customer sees same number the staff sees + + + +# 4. Backend Package + +The backend is a Fastify application running on the Bun runtime. It handles all API requests, payment provider integrations, background job scheduling, and PDF generation. For self-hosted deployments it compiles to a single standalone binary. + + + +## 4.1 Directory Structure + +/packages/backend/src main.ts entry point — registers plugins, starts server /plugins auth.ts JWT verification, role extraction license.ts license loading, module guard database.ts Drizzle connection pool redis.ts Redis connection cors.ts error-handler.ts /routes /v1 accounts.ts rentals.ts lessons.ts repairs.ts inventory.ts payments.ts accounting.ts reports.ts admin.ts webhooks.ts payment processor webhooks health.ts /services business logic — thin routes call services account.service.ts rental.service.ts lesson.service.ts repair.service.ts billing.service.ts accounting.service.ts pdf.service.ts notification.service.ts /db schema/ Drizzle table definitions accounts.ts rentals.ts lessons.ts repairs.ts inventory.ts payments.ts accounting.ts migrations/ Drizzle migration files index.ts db client export /payment-providers base.ts PaymentProvider interface stripe.ts Stripe implementation global-payments.ts Global Payments implementation factory.ts returns correct provider per store /jobs BullMQ workers billing.job.ts daily GP billing scheduler reminders.job.ts renewal and payment reminders backup.job.ts self-hosted backup trigger reports.job.ts scheduled report generation scheduler.ts registers all jobs with cron /license verify.ts Ed25519 signature verification guard.ts requireModule() Fastify hook + + + +## 4.2 Route Pattern + +Routes are thin — they validate input using shared Zod schemas, call a service, and return the result. All business logic lives in services. + + + +// routes/v1/rentals.tsimport { FastifyPluginAsync } from 'fastify'import { RentalCreateSchema } from '@platform/shared'import { RentalService } from '../../services/rental.service'const rentals: FastifyPluginAsync = async (fastify) => { fastify.post('/', { preHandler: [ fastify.authenticate, // JWT check fastify.requireModule('MOD-RENTALS'), // license check fastify.requireRole('staff'), // role check ], schema: { body: RentalCreateSchema, // Zod validation } }, async (request, reply) => { const rental = await RentalService.create( fastify.db, request.companyId, request.body, request.user ) return reply.code(201).send(rental) } ) fastify.get('/:id', { preHandler: [fastify.authenticate], }, async (request, reply) => { const rental = await RentalService.getById( fastify.db, request.companyId, request.params.id ) if (!rental) return reply.code(404).send() return rental } )}export default rentals + + + +## 4.3 Drizzle Schema Example + +// db/schema/rentals.tsimport { pgTable, uuid, numeric, integer, timestamp, boolean, text, pgEnum} from 'drizzle-orm/pg-core'import { accounts } from './accounts'import { inventoryUnits } from './inventory'export const rentalTypeEnum = pgEnum('rental_type', [ 'month_to_month', 'rent_to_own', 'short_term', 'lease_purchase'])export const rentalStatusEnum = pgEnum('rental_status', [ 'active', 'returned', 'cancelled', 'completed'])export const rentals = pgTable('rental', { id: uuid('id').primaryKey().defaultRandom(), company_id: uuid('company_id').notNull(), account_id: uuid('account_id') .notNull() .references(() => accounts.id), inventory_unit_id: uuid('inventory_unit_id') .references(() => inventoryUnits.id), rental_type: rentalTypeEnum('rental_type').notNull(), status: rentalStatusEnum('status') .notNull().default('active'), monthly_rate: numeric('monthly_rate', { precision: 10, scale: 2 }).notNull(), billing_anchor_day: integer('billing_anchor_day').notNull(), billing_group: text('billing_group'), stripe_subscription_id: text('stripe_subscription_id'), rto_purchase_price: numeric('rto_purchase_price', { precision: 10, scale: 2 }), rto_equity_percent: numeric('rto_equity_percent', { precision: 5, scale: 2 }), rto_equity_accumulated: numeric('rto_equity_accumulated', { precision: 10, scale: 2 }) .default('0'), legacy_id: text('legacy_id'), created_at: timestamp('created_at').defaultNow(), updated_at: timestamp('updated_at').defaultNow(),})// Fully typed — TypeScript infers the shape from the schemaexport type Rental = typeof rentals.$inferSelectexport type RentalInsert = typeof rentals.$inferInsert + + + +## 4.4 Payment Provider Interface + +// payment-providers/base.tsexport interface PaymentProvider { readonly name: string // Customers createCustomer(account: Account): Promise deleteCustomer(processorId: string): Promise // Payment methods createSetupSession(): Promise attachPaymentMethod( customerId: string, token: string ): Promise // One-time charges charge(params: ChargeParams): Promise refund(params: RefundParams): Promise // Subscriptions createSubscription( params: SubscriptionParams ): Promise addSubscriptionItem( subscriptionId: string, item: SubscriptionItem ): Promise removeSubscriptionItem( subscriptionId: string, itemId: string ): Promise cancelSubscription(id: string): Promise pauseSubscription(id: string): Promise resumeSubscription(id: string): Promise changeBillingAnchor( subscriptionId: string, day: number ): Promise // Terminal discoverReaders(): Promise collectPayment( params: TerminalPaymentParams ): Promise // Webhooks parseWebhookEvent( payload: string, signature: string ): Promise}// Factory returns correct provider per storeexport function getPaymentProvider( store: Store): PaymentProvider { switch (store.payment_processor) { case 'stripe': return new StripeProvider(store) case 'global_payments': return new GlobalPaymentsProvider(store) default: throw new Error('Unknown processor') }} + + + +## 4.5 BullMQ Billing Scheduler + +// jobs/billing.job.tsimport { Worker, Queue } from 'bullmq'import { getPaymentProvider } from '../payment-providers/factory'export const billingQueue = new Queue('billing', { connection: redis, defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 5000 } }})export const billingWorker = new Worker( 'billing', async (job) => { const { companyId, entityType, entityId } = job.data const store = await StoreService.getById(db, companyId) const provider = getPaymentProvider(store) if (entityType === 'rental') { const rental = await RentalService.getById( db, companyId, entityId ) await provider.charge({ customerId: rental.account.processor_customer_id, amount: rental.monthly_rate, description: `Rental - ${rental.product_name}`, metadata: { rental_id: rental.id } }) await AccountingService.recordRentalPayment(db, rental) } }, { connection: redis, concurrency: 5 })// Scheduler registers daily cron// jobs/scheduler.tsawait billingQueue.add('daily-billing', { trigger: 'cron' }, { repeat: { pattern: '0 0 * * *' } } // midnight daily) + + + +# 5. Bun Runtime + +Bun is a fast JavaScript/TypeScript runtime that executes TypeScript natively without a compilation step. For development this means running the backend directly from .ts files. For self-hosted deployment it compiles to a single standalone binary. + + + +## 5.1 Development + +# Run backend directly — no tsc step neededbun run src/main.ts# Watch mode — restarts on file changesbun --watch run src/main.ts# Run testsbun test + + + +## 5.2 Self-Hosted Binary Compilation + +# Compile to standalone binary# No Bun or Node.js runtime required on target machinebun build src/main.ts \ --compile \ --outfile platform-api \ --target bun-linux-x64 # or bun-linux-arm64# Result: single ~50-80MB executable# Ships inside Docker image — no source code included# Multi-arch build in CI:bun build src/main.ts --compile \ --outfile platform-api-amd64 --target bun-linux-x64bun build src/main.ts --compile \ --outfile platform-api-arm64 --target bun-linux-arm64 + + + +## 5.3 Docker Image + +# DockerfileFROM oven/bun:1 AS builderWORKDIR /buildCOPY packages/shared ./packages/sharedCOPY packages/backend ./packages/backendRUN cd packages/backend && \ bun install --frozen-lockfile && \ bun build src/main.ts \ --compile --outfile /build/platform-api# Runtime image — binary only, no sourceFROM ubuntu:24.04RUN apt-get update && apt-get install -y \ ca-certificates && rm -rf /var/lib/apt/lists/*COPY --from=builder /build/platform-api /app/platform-apiEXPOSE 8000CMD ["/app/platform-api"]# Result:# No TypeScript source in image# No Bun runtime in final image# No node_modules# Just the compiled binary + Ubuntu base + + + +# 6. Drizzle ORM + +Drizzle defines the database schema in TypeScript. All queries are fully typed — TypeScript knows the shape of every query result. Migrations are generated automatically from schema changes. + + + +## 6.1 Typed Queries + +// Fully typed — TypeScript infers return type from schemaconst rental = await db.query.rentals.findFirst({ where: and( eq(rentals.id, id), eq(rentals.company_id, companyId) // multi-tenant scoping ), with: { account: { with: { students: true } }, inventoryUnit: { with: { product: true } } }})// TypeScript knows: rental.account.students[0].first_name// No any, no casting, no guessing// Update with type safetyawait db.update(rentals) .set({ status: 'returned', // TypeScript enforces enum rto_equity_accumulated: '450.00' }) .where(eq(rentals.id, id)) + + + +## 6.2 Migrations + +# Generate migration from schema changesbunx drizzle-kit generate# Apply migrationsbunx drizzle-kit migrate# Migrations are plain SQL files — readable and reviewable# /packages/backend/src/db/migrations/# 0001_initial_schema.sql# 0002_add_repair_parts.sql# 0003_add_lesson_plans.sql# Applied automatically on startup in production# Self-hosted: runs during update process + + + +# 7. Complete Tech Stack + +Layer + +Technology + +Notes + +Language + +TypeScript 5.x + +All packages — API, desktop, mobile, web + +Runtime + +Bun + +Native TS execution, compile-to-binary for self-hosted + +API Framework + +Fastify + +Fast, TypeScript-first, schema validation built in + +ORM + +Drizzle ORM + +TypeScript schema, fully typed queries, SQL-transparent + +Validation + +Zod + +Shared schemas — frontend and backend use same definitions + +Job Queue + +BullMQ + +Redis-based, TypeScript-native, retry logic built in + +Database + +PostgreSQL 16 + +Aurora on SaaS, Docker container on self-hosted + +Cache / Queue broker + +Redis 7 + +Session cache, BullMQ broker, rate limiting + +Desktop + +Electron + React + +Windows/Mac/Linux, imports from shared package + +Mobile + +React Native + +iOS, Stripe Terminal Bluetooth, imports from shared + +Web portals + +React + Vite + +Customer portal + admin panel + +PDF generation + +Puppeteer + +HTML templates rendered to PDF for invoices/reports + +Auth + +Clerk or Auth0 + +JWT-based, RBAC, handles MFA + +Payments — Stripe + +stripe npm package + +Official TypeScript SDK + +Payments — GP + +globalpayments-sdk + +Node.js SDK + +Monorepo + +Turborepo + +Build pipeline, caching, workspace management + +Testing + +Vitest + +Fast, native ESM, compatible with Bun + +Tray app + +Go + +System tray manager for self-hosted — tiny binary + +Installer + +PowerShell → Inno Setup + +Phase 1: PS1 script. Phase 2: .exe wrapper. + + + +# 8. CI/CD Pipeline + +GitHub Actions handles the full build and release pipeline. SaaS deployments push to EKS. Self-hosted releases push compiled binaries to AWS ECR. + + + +On push to main: 1. Typecheck all packages (tsc --noEmit) 2. Lint (ESLint) 3. Test (vitest) 4. Build shared package 5. Bun compile backend → binary (amd64 + arm64) 6. Docker build → push to ECR registry.platform.com/platform:sha-abc123 registry.platform.com/platform:latest 7. Update version registry APIOn tag (release): Same as above plus: 8. Tag Docker image with version registry.platform.com/platform:1.6.0 9. Build Electron desktop app (auto-updates via electron-updater) 10. Build React Native iOS app (TestFlight distribution) 11. Deploy SaaS to EKS (rolling update) 12. Update changelog and release notes + + + +# 9. Local Development Setup + +# Prerequisites: Bun, Docker Desktop, Node.js (for tools)# Clone and installgit clone https://github.com/lunarfront/platformcd platformbun install# Start dependencies (Postgres + Redis)docker compose -f docker-compose.dev.yml up -d# Run database migrationscd packages/backendbunx drizzle-kit migrate# Start all packages in dev modebun run dev # Turborepo starts: # backend localhost:8000 # admin localhost:3001 # web-portal localhost:3002 # desktop Electron window# Run testsbun testbun test --watch \ No newline at end of file diff --git a/planning/18_Implementation_Roadmap.md b/planning/18_Implementation_Roadmap.md new file mode 100644 index 0000000..e636c90 --- /dev/null +++ b/planning/18_Implementation_Roadmap.md @@ -0,0 +1,624 @@ +Forte — Music Store Management Platform + +Implementation Roadmap + +Version 1.0 | Draft + + + +# 1. Purpose + +This document defines the phased implementation order for Forte — a music store management platform built by Lunarfront Tech LLC. Each phase builds on the previous, produces independently testable output, and is scoped for a solo developer working in 2-4 week increments. The goal is a working POS as early as possible, then layer on domain complexity. + +Tech stack: TypeScript / Bun / Fastify / Drizzle ORM / Zod / BullMQ / PostgreSQL 16 / Valkey 8 / Turborepo monorepo. See `17_Backend_Technical_Architecture.md` for full stack details. + +## 1.1 Project Conventions + +Name | Value +App name | Forte +Package namespace | `@forte/shared`, `@forte/backend`, etc. +Database (dev) | `forte` +Database (test) | `forte_test` +Logging | JSON structured logging via Pino (Fastify built-in) +Linting | ESLint + Prettier at monorepo root +Auth (Phase 1-2) | Dev bypass via `X-Dev-User` header — JWT planned, wired in Phase 2 +Auth (Phase 2+) | Self-issued JWTs + bcrypt, swap to Clerk/Auth0 later +Request tracing | Auto-generated request IDs via Fastify (included from Phase 1) +Test strategy | Separate `forte_test` database, reset between test runs +Dev ports | API: 8000, Postgres: 5432, Valkey: 6379 (all exposed to host for dev tooling) +Multi-tenant | `company_id` on all tables for tenant scoping, `location_id` where per-location tracking needed (inventory, transactions, drawer, delivery) + + + +# 2. Phase Summary + +Phase | Name | Dependencies | MVP? +1 | Monorepo Scaffold, Database, Dev Environment | None | Yes +2 | Accounts, Inventory, Auth | Phase 1 | Yes +3 | POS Transactions & Cash Drawer | Phase 2 | Yes +4 | Stripe Integration (Card Payments) | Phase 3 | Yes +5 | Desktop App Shell (Electron) | Phase 4 | Yes +6 | Rentals | Phase 4 | Yes +7 | Lessons | Phase 4 | No +8 | Repairs | Phase 4 | No +9 | Accounting & QuickBooks Export | Phases 6, 7, 8 | No +10 | Licensing & Module Enforcement | Phase 9 | No +11 | Admin Panel (Web) | Phase 10 | No +12 | Customer Portal (Web) | Phase 10 | No +13 | Batch Repairs & Delivery | Phase 8 | No +14 | Advanced Billing | Phases 6, 7 | No +15 | Mobile App (iOS) | Phase 4 | No +16 | Self-Hosted Installer | Phase 10 | No +17 | AIM Migration | Phase 2 | No +18 | Personnel (Time Clock, Scheduling, Time Off) | Phase 2 | No + +MVP for beta store: Phases 1–6 (scaffold through rentals + desktop app). + +Phases 5–8 are largely independent after Phase 4 and can be interleaved. Phases 11–13 are also independent. Phase 18 (Personnel) can be built any time after Phase 2 — it only needs employee/user tables. + + + +# 3. Phase Details + +## 3.1 Phase 1 — Monorepo Scaffold, Database, Dev Environment + +Reference docs: `01_Overall_Architecture.md`, `17_Backend_Technical_Architecture.md` + +### Goal + +A running Turborepo monorepo with Docker Compose dev environment, Fastify server connected to Postgres and Redis, a health check endpoint, and the shared package exporting its first types. + +### Deliverables + +Area | Files / Artifacts +Root config | `turbo.json`, root `package.json` (workspaces), `tsconfig.base.json`, `.env.example`, `CLAUDE.md` +Linting | `.eslintrc.cjs`, `.prettierrc`, root lint/format scripts in Turborepo pipeline +Docker | `docker-compose.dev.yml` — PostgreSQL 16 (`forte` database) + Valkey 8, ports exposed to host +Shared package | `packages/shared/package.json` (`@forte/shared`), `packages/shared/src/types/index.ts`, `packages/shared/src/schemas/index.ts`, `packages/shared/src/utils/currency.ts`, `packages/shared/src/utils/dates.ts` +Backend package | `packages/backend/package.json` (`@forte/backend`), `packages/backend/src/main.ts` (Fastify entry with Pino JSON logging + request ID tracing), `packages/backend/src/plugins/database.ts` (Drizzle connection), `packages/backend/src/plugins/redis.ts`, `packages/backend/src/plugins/error-handler.ts`, `packages/backend/src/plugins/cors.ts`, `packages/backend/src/plugins/dev-auth.ts` (dev bypass via `X-Dev-User` header), `packages/backend/src/routes/v1/health.ts` +Database | `packages/backend/src/db/index.ts` (client export), `packages/backend/src/db/schema/companies.ts` (company table — the tenant anchor), `packages/backend/src/db/schema/locations.ts` (location table — physical store locations), `drizzle.config.ts` +Seed script | `packages/backend/src/db/seed.ts` — creates a test company + location + admin user for local dev +Testing | `vitest.config.ts`, health endpoint integration test, `forte_test` database for test isolation + +### Architecture Decisions Settled + +- `company_id` (tenant) + `location_id` (physical store) scoping pattern established on the first domain tables +- Inventory and transaction tables use both `company_id` and `location_id`; other tables use `company_id` only +- Drizzle migration workflow (generate → migrate) +- Shared package import convention (`@forte/shared`) +- Standardized error response format (consistent JSON shape for errors) +- Request context pattern (`companyId`, `locationId`, and `user` on request) +- JSON structured logging via Pino with request ID on every log line +- Dev auth bypass (`X-Dev-User` header) — replaced by real JWT auth in Phase 2 + +### Testable Outcome + +`bun run dev` starts Fastify on port 8000. `GET /v1/health` returns `{ status: "ok", db: "connected", redis: "connected" }`. + + + +## 3.2 Phase 2 — Accounts, Inventory, Auth + +Reference docs: `02_Domain_Accounts_Customers.md`, `03_Domain_Inventory.md` (sale inventory only) + +### Goal + +The foundational domain entities that every other feature depends on — accounts, members, products, inventory units, categories, suppliers. Plus authentication so routes are protected. + +### Deliverables + +Area | Files / Artifacts +Auth plugin | `packages/backend/src/plugins/auth.ts` — self-issued JWTs + bcrypt (swap to Clerk/Auth0 later) +User/Employee schema | `packages/backend/src/db/schema/users.ts` — `user` table with `company_id`, role enum (admin, manager, staff, technician, instructor), hashed password +Account domain | `packages/backend/src/db/schema/accounts.ts` — `account`, `member`, `account_payment_method` tables +Account routes | `packages/backend/src/routes/v1/accounts.ts` — CRUD + search (name, phone, email, account_number) +Account service | `packages/backend/src/services/account.service.ts` +Inventory domain | `packages/backend/src/db/schema/inventory.ts` — `product`, `inventory_unit`, `category` tables +Inventory routes | `packages/backend/src/routes/v1/inventory.ts` — CRUD, search by SKU/UPC/name, stock level queries +Inventory service | `packages/backend/src/services/inventory.service.ts` +Supplier schema | `packages/backend/src/db/schema/suppliers.ts` — `supplier` table +Shared types/schemas | `packages/shared/src/types/account.ts`, `packages/shared/src/schemas/account.schema.ts`, `packages/shared/src/types/inventory.ts`, `packages/shared/src/schemas/inventory.schema.ts` + +### Business Rules Enforced + +- Every query scoped by `company_id`; inventory queries also scoped by `location_id` +- Account soft-delete only — financial history must be retained +- Duplicate account detection on email and phone during creation +- Product `is_serialized` flag controls whether `inventory_unit` records are required +- Legacy fields (`legacy_id`, `legacy_source`, `migrated_at`) present on all domain tables from day one + +### Testable Outcome + +Full CRUD on accounts, members, products, and inventory via authenticated API calls. Account search by multiple fields works. + + + +## 3.3 Phase 3 — POS Transactions & Cash Drawer + +Reference docs: `07_Domain_Sales_POS.md` + +### Goal + +A working POS transaction loop — create a transaction, add line items, apply discounts, calculate tax, complete with cash payment. Cash drawer open/close with over/short tracking. This is the minimum viable POS. + +### Deliverables + +Area | Files / Artifacts +Transaction domain | `packages/backend/src/db/schema/transactions.ts` — `transaction` (with `company_id` + `location_id`), `transaction_line_item`, `discount`, `discount_audit` tables +Transaction routes | `packages/backend/src/routes/v1/transactions.ts` — create, add line items, apply discount, complete, void, refund +Transaction service | `packages/backend/src/services/transaction.service.ts` — tax calculation, discount application, inventory decrement on completion +Cash drawer | `packages/backend/src/db/schema/drawer.ts` — `drawer_session` table (open, close, denominations, over/short) +Drawer routes | `packages/backend/src/routes/v1/drawer.ts` +Shared types/schemas | `packages/shared/src/types/payment.ts`, `packages/shared/src/schemas/transaction.schema.ts` + +### Business Rules Enforced + +- Discounts above manager threshold require approval — logged in `discount_audit` +- Manual price overrides treated as discounts with reason code +- Inventory `qty_on_hand` decremented on sale (non-serialized) or status changed to `sold` (serialized) +- Transaction lifecycle: `pending` → `completed` / `voided` +- Refund creates a new transaction of type `refund` linked to original +- Discount audit records are immutable + +### Testable Outcome + +API-driven POS flow: open drawer, create transaction, add line items from inventory, apply discount, complete with cash, close drawer with denomination count, see over/short. Inventory quantities updated. + + + +## 3.4 Phase 4 — Stripe Integration (Card Payments) + +Reference docs: `08_Domain_Payments_Billing.md`, `07_Domain_Sales_POS.md` + +### Goal + +Card-present and card-keyed payments via Stripe Terminal and Stripe Elements. Webhook handling for payment confirmation. This turns the cash-only POS into a real POS. + +### Deliverables + +Area | Files / Artifacts +Payment provider interface | `packages/backend/src/payment-providers/base.ts` — `PaymentProvider` interface (supports future Global Payments addition) +Stripe provider | `packages/backend/src/payment-providers/stripe.ts` — charge, refund, terminal reader discovery, terminal payment collection, setup session +Provider factory | `packages/backend/src/payment-providers/factory.ts` +Webhook handler | `packages/backend/src/routes/v1/webhooks.ts` — `payment_intent.succeeded`, `payment_intent.payment_failed` +Webhook storage | `packages/backend/src/db/schema/webhooks.ts` — `stripe_webhook_event` table (store-before-process pattern) +Stripe customer sync | Account creation triggers Stripe Customer creation, `stripe_customer_id` stored on account + +### Architecture Notes + +The `PaymentProvider` interface is defined now even though only Stripe is implemented. Global Payments (`PAY-GP`) can be added later without changing business logic. + +Critical design difference: Stripe manages recurring billing via its Subscriptions API (provider-managed). Global Payments only provides tokenized charge capability — the platform must own the billing schedule via BullMQ cron jobs (platform-managed). The interface exposes `managedSubscriptions: boolean` so the billing service knows whether to delegate or self-manage. For GP stores, the platform also handles proration calculations internally, whereas Stripe calculates proration automatically. See `08_Domain_Payments_Billing.md` section 6 for full details. + +### Testable Outcome + +Create a transaction, complete with Stripe Terminal (test mode reader) or Stripe Elements. Webhook confirms payment. Transaction marked completed. Refund via Stripe works. + + + +## 3.5 Phase 5 — Desktop App Shell (Electron) + +Reference docs: `01_Overall_Architecture.md`, `17_Backend_Technical_Architecture.md` + +### Goal + +An Electron app that runs the POS. Not feature-complete — just the shell with navigation, auth login, and the POS transaction screen wired to the API. This is the first time a human can use the system. + +### Deliverables + +Area | Files / Artifacts +Desktop package | `packages/desktop/package.json` — Electron + React + Vite +App shell | Main window, menu bar, navigation sidebar +Auth screen | Login form, JWT storage, auto-refresh +POS screen | Product search/scan, cart, discount application, payment method selection (cash/card), complete transaction +Customer lookup | Search by name/phone/email, attach account to transaction +Inventory browse | Product list, stock levels, basic filters +API client | Shared API client layer (`packages/shared/src/api/client.ts` or generated via `tools/codegen`) — reused by all React apps + +### Testable Outcome + +A human can log in, search inventory, ring up a sale, take cash or card payment, and see the transaction complete. + + + +## 3.6 Phase 6 — Rentals + +Reference docs: `04_Domain_Rentals.md`, `08_Domain_Payments_Billing.md`, `11_Domain_Billing_Date_Management.md` + +### Goal + +Full rental lifecycle — create contracts (all four types), track rental fleet, handle deposits, process returns. Recurring billing via Stripe Subscriptions. Rent-to-own equity tracking and buyout. + +### Deliverables + +Area | Files / Artifacts +Rental domain | `packages/backend/src/db/schema/rentals.ts` — `rental`, `rental_payment` tables +Rental routes | `packages/backend/src/routes/v1/rentals.ts` — create, return, buyout, list by account +Rental service | `packages/backend/src/services/rental.service.ts` +Billing service | `packages/backend/src/services/billing.service.ts` — Stripe subscription create, add/remove line items, billing group consolidation +RTO business logic | `packages/shared/src/business-logic/rto.ts` — equity calculation, buyout amount +Webhook additions | `invoice.paid`, `invoice.payment_failed`, `customer.subscription.deleted` handlers +Billing anchor | `billing_anchor_day` column on rental, capped at 28, change audit log +Desktop UI | Rental intake form, rental list, return workflow, RTO buyout screen + +### Business Rules Enforced + +- Billing group consolidation vs split billing +- RTO equity accumulation per payment +- Deposit collected at start (tracked as liability) +- Return updates `inventory_unit` status to `available` or `in_repair` +- Mid-cycle rental add prorates via Stripe +- Billing anchor day requests for 29th/30th/31st capped to 28th + +### Testable Outcome + +Create a rental, see Stripe subscription created (test mode). Simulate payment via webhook. See equity accumulate on RTO. Process return. Process buyout. + + + +## 3.7 Phase 7 — Lessons + +Reference docs: `05_Domain_Lessons.md` + +### Goal + +Instructor management, scheduling, enrollments, attendance tracking, makeup credits. Recurring billing via Stripe Subscriptions. Session notes, homework, grading scales, and lesson plans. + +### Deliverables + +Area | Files / Artifacts +Lesson domain | `packages/backend/src/db/schema/lessons.ts` — `instructor`, `lesson_type`, `schedule_slot`, `enrollment`, `lesson_session` tables +Grading | `grading_scale`, `grading_scale_level` tables + default seed data (Standard Letter, Numeric 1-10, ABRSM Style, Progress, Concert Readiness, Simple) +Lesson plans | `member_lesson_plan`, `lesson_plan_section`, `lesson_plan_item`, `lesson_plan_item_grade_history`, `lesson_session_plan_item` tables +Templates | `lesson_plan_template`, `lesson_plan_template_section`, `lesson_plan_template_item` tables +Lesson routes | `packages/backend/src/routes/v1/lessons.ts` — enrollment CRUD, session CRUD, attendance, lesson plan CRUD, grading +Lesson service | `packages/backend/src/services/lesson.service.ts` +Billing | Enrollment creates Stripe subscription (or adds line item to existing) — reuses billing service from Phase 6 +Desktop UI | Schedule view, enrollment form, attendance marking, post-lesson notes form, lesson plan editor + +### Business Rules Enforced + +- No double-booking (student or instructor at same day/time) +- Group lesson `max_students` cap on `schedule_slot` +- Makeup credits linked to original missed session via `makeup_for_session_id` +- Only one active lesson plan per enrollment at a time +- `instructor_notes` never exposed to student/parent API routes +- Scale levels used in grade history cannot be deleted — only deactivated +- Skipped items excluded from progress percentage + +### Testable Outcome + +Create instructor, define schedule, enroll student, mark attendance, record session notes with grades, see lesson plan progress percentage. + + + +## 3.8 Phase 8 — Repairs + +Reference docs: `06_Domain_Repairs.md`, `03_Domain_Inventory.md` (repair parts sections) + +### Goal + +Full individual repair ticket lifecycle from intake through pickup/payment. Parts logging, labor tracking, flat-rate services. Repair parts inventory separate from sale inventory. + +### Deliverables + +Area | Files / Artifacts +Repair domain | `packages/backend/src/db/schema/repairs.ts` — `repair_ticket`, `repair_line_item` tables +Repair parts | `packages/backend/src/db/schema/repair-parts.ts` — `repair_part`, `repair_part_usage_template`, `repair_part_usage` tables +Repair routes | `packages/backend/src/routes/v1/repairs.ts` — full lifecycle CRUD, parts logging, estimate generation +Repair service | `packages/backend/src/services/repair.service.ts` +Repair payment | Payment at pickup integrates with transaction system (Phase 3) +Seed data | Default bow hair usage templates (full size, cello, bass, fractional sizes) +Desktop UI | Repair intake form, technician work screen with parts logging, repair queue/list, pickup/payment flow + +### Business Rules Enforced + +- Status lifecycle with required transitions (intake → diagnosing → pending_approval → approved → in_progress → ready → picked_up) +- Estimate approval required before work begins (waivable with manager override) +- Parts cost recorded at time of use — historical accuracy preserved +- Technician cannot log more parts than `qty_on_hand` +- Dual-use parts decrement sale inventory `qty_on_hand` +- Shop supply parts tracked for cost but never appear on customer invoice +- Flat-rate services bundle labor + material into single invoice line item +- Actual cost variance from estimate requires reason code in audit trail + +### Testable Outcome + +Create repair ticket, log labor and parts, generate estimate, approve, mark complete, collect payment at pickup. Repair parts inventory decremented. + + + +## 3.9 Phase 9 — Accounting & QuickBooks Export + +Reference docs: `12_Domain_Accounting_Journal_Entries.md` + +### Goal + +Automatic double-entry journal entry generation for every financial event. Store-configurable chart of accounts matching QuickBooks. CSV export for QB import. In-platform financial reports. + +### Deliverables + +Area | Files / Artifacts +Accounting domain | `packages/backend/src/db/schema/accounting.ts` — `account_code`, `journal_entry`, `journal_entry_line`, `export_batch` tables +JE generation | `packages/backend/src/services/accounting.service.ts` — functions for each event type (cash sale, card sale, rental payment, RTO payment/buyout, lesson payment, repair payment, deposits, refunds, discounts, drawer variance, proration, inventory purchase, bad debt write-off) +JE rules | `packages/shared/src/business-logic/accounting.ts` — journal entry mapping rules (which accounts debit/credit per event) +Export | `packages/backend/src/routes/v1/accounting.ts` — export by date range (CSV), mark exported, reconciliation confirmation +Seed data | Default chart of accounts (assets 1000–1320, liabilities 2000–2400, revenue 4000–4500, contra 4900–4920, COGS 5000–5100, expenses 6000–6300) +Reports | Revenue summary, gross vs net, AR aging, deferred revenue, cash flow, Stripe clearing reconciliation, cash over/short history, COGS vs revenue + +### Architecture Note + +This phase retrofits accounting hooks into all previously built financial flows. `AccountingService.record*()` calls are added to transaction, rental, lesson, and repair services. Deferred to this phase intentionally — operational flows must be correct before accounting hooks are layered on. + +### Business Rules Enforced + +- `total_debits` must equal `total_credits` — entries that don't balance are rejected +- Entry date cannot be future, and past > 30 days requires manager override +- Voided entries generate reversing entries (equal and opposite) — never deleted +- Exported entries excluded from subsequent exports unless manager override +- Chart of accounts `quickbooks_account_name` must match QB exactly + +### Testable Outcome + +Complete a sale, see journal entry generated automatically. Export date range as CSV. Verify debits equal credits on every entry. AR aging report shows outstanding balances. + + + +## 3.10 Phase 10 — Licensing & Module Enforcement + +Reference docs: `15_Licensing_Modules_Pricing.md` + +### Goal + +Ed25519 signed license files control which modules are active. API route guards block unlicensed routes. UI shows upgrade prompts for locked modules. Trial licenses enable 30-day evaluation. + +### Deliverables + +- `packages/backend/src/license/verify.ts` — Ed25519 signature verification, public key embedded at build time +- `packages/backend/src/license/guard.ts` — `requireModule()` Fastify preHandler hook +- `packages/backend/src/plugins/license.ts` — loads license on startup, caches in memory +- `packages/shared/src/types/license.ts` — `License`, `LicenseModule` types +- `packages/shared/src/constants/modules.ts` — `MODULE_IDS` enum (CORE, MOD-RENTALS, MOD-LESSONS, MOD-REPAIRS, etc.) +- `packages/shared/src/business-logic/license.ts` — `hasModule()` function for frontend checks +- Trial license API endpoint — generates 30-day trial for any unlicensed module +- Shared React upgrade prompt component for unlicensed module screens + +### Testable Outcome + +API route returns 403 with upgrade prompt when module not licensed. UI shows upgrade prompt instead of module content. Trial license enables module for 30 days. + + + +## 3.11 Phase 11 — Admin Panel (Web) + +Reference docs: `01_Overall_Architecture.md` + +### Deliverables + +- `packages/admin/` — React + Vite web app +- User management — CRUD for employee accounts, role assignment +- Store settings — store name, address, tax configuration, timezone, billing preferences +- License management — upload license file, view active modules, start trials +- Audit log viewer — searchable log of discount audits, billing changes, refunds, write-offs +- Reporting dashboards — sales summary, rental/lesson enrollment counts, repair queue metrics +- `packages/backend/src/routes/v1/admin.ts` — admin-only endpoints + + + +## 3.12 Phase 12 — Customer Portal (Web) + +Reference docs: `05_Domain_Lessons.md` (parent portal sections) + +### Deliverables + +- `packages/web-portal/` — React + Vite web app +- Separate customer auth flow (email/password or magic link) — scoped to account data only +- Lesson view — enrollment summary, upcoming lessons, session notes (student-facing only), homework, lesson plan progress, grade history +- Repair status — ticket status tracking, estimated completion date +- Billing — invoice history, payment history, online payment via Stripe Elements +- `packages/backend/src/routes/v1/portal.ts` — customer-scoped endpoints (never expose instructor private notes) + + + +## 3.13 Phase 13 — Batch Repairs & Delivery + +Reference docs: `09_Domain_Batch_Repairs.md`, `10_Domain_Delivery_Chain_of_Custody.md` + +### Deliverables + +- `repair_batch` table — batch lifecycle (scheduled → picked_up → intake_complete → pending_approval → approved → in_progress → ready → delivered → invoiced → paid → closed) +- Batch approval cascading to all child `repair_ticket` records +- Batch invoicing — itemized per instrument with work, parts, labor +- `delivery_event`, `delivery_event_item` tables — condition tracking at every handoff, signature capture (base64), photo URLs (S3) +- School PO number on batch and invoice +- Desktop UI for batch intake, delivery scheduling, batch invoice generation +- Delivery completion auto-updates linked repair ticket and batch status + +### Business Rules + +- Batch status auto-advances to `ready` when all child tickets reach `ready` or `cancelled` +- Instrument count discrepancy flagged at pickup — requires staff acknowledgement +- Missing instrument (`was_transferred = false`) triggers manager alert +- Signature required for `store_pickup` and `store_delivery` events + + + +## 3.14 Phase 14 — Advanced Billing + +Reference docs: `11_Domain_Billing_Date_Management.md` + +### Deliverables + +- `billing_anchor_change_log` table — append-only audit +- Billing anchor change routes — preview proration, execute change, bulk preview, bulk execute, history +- Proration logic — earlier day (credit), later day (charge), consolidation, splitting +- Edge cases — 48hr pending invoice window warning, failed payment blocks anchor change, paused subscription (no proration), RTO equity unaffected +- Bulk change — grouped under `bulk_change_id`, sequential Stripe API calls, full rollback on partial failure +- Bulk change preview screen showing per-subscription proration breakdown + + + +## 3.15 Phase 15 — Mobile App (iOS) + +Reference docs: `01_Overall_Architecture.md` + +### Deliverables + +- `packages/mobile/` — React Native iOS app +- Convention POS — product search, cart, payment via Stripe Terminal Bluetooth reader +- Customer lookup +- Delivery workflow — pull up delivery event, check off instruments, capture condition/photos/signature +- Offline queue — local SQLite for transactions when offline, sync on reconnect +- Convention cash drawer (account 1010) + + + +## 3.16 Phase 16 — Self-Hosted Installer + +Reference docs: `14_Self_Hosted_Installer_Platform_Compatibility.md`, `16_Windows_Installer_PowerShell.md` + +### Deliverables + +- Production `docker-compose.yml` — api, postgres, valkey, admin (nginx), portal (nginx), nginx (reverse proxy), updater, backup services +- `Dockerfile` — multi-stage build: compile backend with `bun build --compile`, copy binary to clean Ubuntu 24.04 image +- `tools/installer/install.ps1` — phased PowerShell installer with Hyper-V/WSL2 detection, Docker install, resume-after-reboot via registry Run key, NSSM service registration +- `tools/installer/tray-app/` — Go system tray app with health monitoring (green/yellow/orange/red), update checker, manual backup trigger, log viewer +- Automated backup service — daily pg_dump, 30-day retention, integrity verification +- Update registry API — `GET /check?version=X&license=Y` returns latest version info +- macOS `.dmg` with menu bar app +- Linux one-line install script with systemd service + + + +## 3.17 Phase 17 — AIM Migration + +Reference docs: `01_Overall_Architecture.md` (migration sections), `08_Domain_Payments_Billing.md` (payment transition) + +### Deliverables + +- `tools/migration/` — TypeScript ETL scripts connecting directly to AIM MSSQL database +- Per-domain extraction scripts — accounts/customers, inventory, open rentals, active lessons, repair history +- Data validation — dry-run mode, exception logging, manual review of edge cases +- Duplicate detection — run before import, staff reviews conflicts +- Legacy tagging — all migrated records tagged with `legacy_id`, `legacy_source = 'aim'`, `migrated_at` +- Payment transition — `requires_payment_update = true` on migrated payment methods, staff prompted to collect new card at each renewal +- Old processor wind-down — 120-day window after last transaction before closing old processor account + + + +## 3.18 Phase 18 — Personnel (Time Clock, Scheduling, Time Off) + +Reference docs: `19_Domain_Personnel.md` + +### Goal + +Employee records, time clock with location tracking, work schedules, time-off requests with approval workflow, overtime calculation, and payroll export. This is a licensed module (MOD-PERSONNEL). + +### Deliverables + +- `employee`, `time_clock_entry`, `schedule`, `schedule_recurring_template`, `time_off_request`, `time_off_balance` tables +- Employee CRUD with multi-role support (staff, technician, instructor, manager, admin) +- Time clock routes — clock in/out, manager edit with audit trail +- Schedule management — recurring templates, concrete entries, conflict detection +- Time-off workflow — request, approve/deny, balance tracking, accrual +- Overtime calculation based on configurable rules (weekly/daily thresholds) +- Payroll CSV export for external payroll services +- Integration hooks: instructor_id and technician_id reference employee table + +### Business Rules + +- Employee can work at any location (not locked to one) +- Clock entries cannot overlap — no simultaneous clock-ins +- Auto clock-out at midnight if forgotten, flagged for manager review +- Per-lesson instructors tracked per session, not via time clock +- Terminated employees retain all historical records + +### Testable Outcome + +Create employee, clock in/out at a location, create schedule, submit and approve time-off request, generate payroll export CSV. + + + +# 4. Dependency Map + +``` +P1 Scaffold → P2 Accounts/Inventory → P3 POS → P4 Stripe + | | + P18 +-------+-------+-------+ + Personnel | | | | + P5 P6 P7 P8 + Desktop Rentals Lessons Repairs + | | | | + +-------+---+---+-------+ + | + P9 Accounting + | + P10 Licensing + | + +-------+---+---+-------+ + | | | + P11 P12 P13 + Admin Portal Batch/Delivery + | | | + +-------+-------+-------+ + | + P14 Adv Billing + | + +-----+-----+ + | | + P15 P16 + Mobile Installer + | + P17 + Migration +``` + + + +# 5. MVP Definition + +The minimum viable product for a beta store deployment is Phases 1–6: + +- Monorepo + dev environment (Phase 1) +- Accounts + inventory + auth (Phase 2) +- POS with cash payments (Phase 3) +- Card payments via Stripe (Phase 4) +- Desktop Electron app (Phase 5) +- Instrument rentals with recurring billing (Phase 6) + +This covers the most common daily operations — a store can use it alongside AIM for new transactions while legacy data remains in AIM. + + + +# 6. Verification Strategy + +After each phase: + +- Run `bun test` — all tests pass +- `docker compose -f docker-compose.dev.yml up -d` — all services healthy +- Hit API endpoints via curl or httpie to verify CRUD operations work +- After Phase 5+: manual testing through desktop app +- After Phase 9: verify every journal entry has `total_debits = total_credits` +- After Phase 16: clean install on a fresh Windows machine, platform running within 30 minutes + + + +# 7. Cross-Cutting Concerns Not Yet Addressed + +The following items are referenced across multiple planning documents but do not have dedicated domain docs. They should be designed before or during their dependent phase: + +Concern | Needed By | Notes +Tax configuration | Phase 3 | Sales tax collected at POS but no tax rate config, jurisdiction handling, or TaxJar/Avalara integration designed +Notification system | Phase 6 | Repairs, billing, and lessons reference "notify customer" but no channel definitions, preferences schema, or template system exists +Offline sync strategy | Phase 15 | iOS app and desktop both reference offline queuing but conflict resolution is not designed +Search/indexing | Phase 2 | Account/customer lookup described but no full-text search, trigram indexes, or search infrastructure specified +Cash drawer schema | Phase 3 | Mentioned in POS and accounting docs but no `drawer_session` table defined in any planning doc +Frontend themes | Phase 5 | All frontend apps should support themes with per-user preference storage + +### Resolved + +- Employee/user table schema → now covered by `19_Domain_Personnel.md` (Phase 18) +- `store` entity ambiguity → resolved as `company` (tenant) + `location` (physical store) +- `student` entity naming → renamed to `member` to support multiple adults per account +- Redis → replaced with Valkey 8 (Redis-compatible fork) +- Payment provider billing ownership → documented in `08_Domain_Payments_Billing.md` section 6 (Stripe = provider-managed, GP = platform-managed) diff --git a/planning/19_Domain_Personnel.md b/planning/19_Domain_Personnel.md new file mode 100644 index 0000000..531725e --- /dev/null +++ b/planning/19_Domain_Personnel.md @@ -0,0 +1,263 @@ +Forte — Music Store Management Platform + +Domain Design: Personnel Management + +Version 1.0 | Draft + + + +# 1. Overview + +The Personnel domain manages employee records, time clock, time-off requests, and work schedules. This is foundational infrastructure — every other domain references employees (POS operators, technicians, instructors, drivers). This domain provides the employee entity and workforce management tools. + +Personnel management is a licensed module (MOD-PERSONNEL). Without it, the platform still tracks basic user accounts for login and role-based access. The module adds time clock, scheduling, time-off, and labor reporting. + + + +# 2. Core Concepts + +## 2.1 Employee + +An employee is a person who works for the company. They have a user account for system login and an employee record for HR/scheduling purposes. An employee can work at any location within the company — they are not locked to a single location. + +- Linked to a `user` account for authentication +- Has a home location (primary work site) but can clock in at any location +- Roles: admin, manager, staff, technician, instructor +- An employee can hold multiple roles (e.g. a technician who also teaches lessons) +- Pay rate tracked for labor cost calculations (repair labor, lesson instructor pay) +- Employment status: active, inactive, terminated + +## 2.2 Time Clock + +Employees clock in and out to track hours worked. Clock entries record which location the employee is working at, supporting multi-location operations. + +- Clock in/out via desktop app or web UI +- Location recorded at clock-in — employee may work at different locations on different days +- Break tracking — paid and unpaid breaks +- Overtime calculated based on configurable rules (weekly threshold, daily threshold) +- Clock entries editable by managers with audit trail + +## 2.3 Schedules + +Work schedules define when employees are expected to work. Schedules are per-location and per-week. + +- Weekly recurring schedules with override capability for specific dates +- Shift-based — start time, end time, location, role +- Instructors have dual scheduling: lesson schedule (from Lessons domain) + store shift schedule +- Schedule conflicts detected — cannot schedule same employee at two locations simultaneously +- Published schedules visible to employees (future: via portal or mobile) + +## 2.4 Time Off + +Employees can request time off. Managers approve or deny. Approved time off blocks scheduling for those dates. + +- Request types: vacation, sick, personal, unpaid +- Accrual tracking — configurable accrual rates per type +- Manager approval workflow with notification +- Approved time off appears on schedule as blocked +- Annual carryover rules configurable per type + + + +# 3. Database Schema + +## 3.1 employee + +Column | Type | Notes +id | uuid PK | +company_id | uuid FK | Tenant scoping +user_id | uuid FK | Linked login account — nullable for employees not yet set up with system access +first_name | varchar | +last_name | varchar | +email | varchar | Work email +phone | varchar | +home_location_id | uuid FK | Primary work location +roles | text[] | Array of roles: admin, manager, staff, technician, instructor +hire_date | date | +termination_date | date | Nullable — set when terminated +employment_status | enum | active, inactive, terminated +pay_type | enum | hourly, salary, commission, per_lesson +pay_rate | numeric(10,2) | Hourly rate or salary amount +overtime_eligible | boolean | +notes | text | Internal HR notes +legacy_id | varchar | AIM employee ID +created_at | timestamptz | +updated_at | timestamptz | + +## 3.2 time_clock_entry + +Column | Type | Notes +id | uuid PK | +company_id | uuid FK | +employee_id | uuid FK | +location_id | uuid FK | Where the employee clocked in +clock_in | timestamptz | +clock_out | timestamptz | Nullable — null while clocked in +break_minutes | integer | Total break time in minutes +break_type | enum | paid, unpaid, none +total_hours | numeric(6,2) | Computed: clock_out - clock_in - unpaid breaks +is_overtime | boolean | Flagged based on overtime rules +overtime_hours | numeric(6,2) | Hours beyond overtime threshold +edited | boolean | True if entry was modified after clock-out +edited_by | uuid FK | Manager who edited +edit_reason | text | Required if edited +created_at | timestamptz | + +## 3.3 schedule + +Column | Type | Notes +id | uuid PK | +company_id | uuid FK | +employee_id | uuid FK | +location_id | uuid FK | +schedule_date | date | Specific date for this shift +start_time | time | +end_time | time | +role | varchar | What role for this shift (staff, technician, instructor) +is_recurring | boolean | True if generated from a recurring template +recurring_template_id | uuid FK | Nullable — links to the template that generated this +notes | text | +created_by | uuid FK | Manager who created +created_at | timestamptz | +updated_at | timestamptz | + +## 3.4 schedule_recurring_template + +Weekly recurring schedule patterns. The system generates concrete `schedule` entries from these templates. + +Column | Type | Notes +id | uuid PK | +company_id | uuid FK | +employee_id | uuid FK | +location_id | uuid FK | +day_of_week | integer | 0=Sunday through 6=Saturday +start_time | time | +end_time | time | +role | varchar | +effective_from | date | When this pattern starts +effective_until | date | Nullable — open-ended if null +is_active | boolean | +created_at | timestamptz | + +## 3.5 time_off_request + +Column | Type | Notes +id | uuid PK | +company_id | uuid FK | +employee_id | uuid FK | +request_type | enum | vacation, sick, personal, unpaid +start_date | date | +end_date | date | +total_days | numeric(4,1) | Supports half days +status | enum | pending, approved, denied, cancelled +reason | text | Employee's reason +manager_notes | text | Manager's response +reviewed_by | uuid FK | Manager who reviewed +reviewed_at | timestamptz | +created_at | timestamptz | + +## 3.6 time_off_balance + +Tracks accrued and used time off per employee per type per year. + +Column | Type | Notes +id | uuid PK | +company_id | uuid FK | +employee_id | uuid FK | +year | integer | Calendar year +request_type | enum | vacation, sick, personal +accrued | numeric(5,1) | Days accrued this year +used | numeric(5,1) | Days used this year +carried_over | numeric(5,1) | Days carried from previous year +available | numeric(5,1) | Computed: accrued + carried_over - used +created_at | timestamptz | +updated_at | timestamptz | + + + +# 4. Overtime Rules + +Overtime configuration is per-company, stored in company settings. + +Setting | Default | Notes +weekly_overtime_threshold | 40 | Hours per week before overtime kicks in +daily_overtime_threshold | null | Nullable — some states require daily overtime (e.g. California = 8 hrs) +overtime_multiplier | 1.5 | Pay multiplier for overtime hours +double_time_threshold | null | Nullable — hours per day before double time (e.g. California = 12 hrs) +double_time_multiplier | 2.0 | Pay multiplier for double time + +Overtime is calculated at clock-out based on the current week's total hours. If daily thresholds are configured, both daily and weekly are evaluated and the higher overtime amount applies. + + + +# 5. Key Workflows + +## 5.1 Clock In / Out + +- Employee opens desktop app or web UI +- Selects "Clock In" — system records current time, location, and employee +- On clock out, system records end time, calculates total hours and break time +- If total weekly hours exceed overtime threshold, overtime flagged +- Manager can edit clock entries with required reason — logged in audit trail + +## 5.2 Schedule Creation + +- Manager creates recurring schedule templates for each employee +- System generates concrete schedule entries for upcoming weeks +- Manager can override specific dates (e.g. swap shifts, add extra coverage) +- Instructor lesson schedule slots (from Lessons domain) display alongside store shifts for visibility +- Schedule conflicts flagged — same employee at two locations at the same time + +## 5.3 Time Off Request + +- Employee submits request specifying dates, type, and reason +- Manager receives notification +- Manager approves or denies with optional notes +- Approved time off deducts from employee's time_off_balance +- Schedule entries for approved dates are flagged or removed +- If employee is an instructor, lesson sessions for those dates can be auto-cancelled or flagged for makeup + +## 5.4 Pay Period Reporting + +- Manager selects date range for pay period +- System generates report: employee, regular hours, overtime hours, total hours +- Export to CSV for payroll processing (platform does not run payroll — exports data for external payroll service) +- Includes time-off hours taken by type + + + +# 6. Integration with Other Domains + +Domain | Integration +Lessons | Instructors are employees. Lesson schedule_slot.instructor_id references employee. Time off auto-flags affected lesson sessions. +Repairs | Technicians are employees. repair_ticket.assigned_technician_id references employee. Labor cost calculated from employee pay_rate. +Sales/POS | transaction.processed_by references employee. Drawer sessions linked to employee. +Delivery | delivery_event.driver_employee_id references employee. +Accounting | Labor costs from repair tickets use employee.pay_rate for margin calculation. Payroll export supports journal entry generation for labor expense. + + + +# 7. Business Rules + +- Employee must have unique email within company +- Clock entries cannot overlap — cannot be clocked in at two locations simultaneously +- Clock-out required before new clock-in (system auto-clocks out at midnight if forgotten — flagged for manager review) +- Time-off requests for past dates require manager approval +- Schedule recurring templates auto-generate entries 4 weeks ahead (configurable) +- Terminated employees retain all historical records (time clock, schedules, time off) — never deleted +- Employee pay_rate changes are effective immediately — historical clock entries retain the rate at time of clock-out +- Per-lesson instructors: pay tracked per lesson session attended, not via time clock + + + +# 8. Reporting + +Report | Description +Hours summary | Total regular + overtime hours per employee per period +Overtime report | Employees who hit overtime threshold — broken out by daily vs weekly +Time-off balances | Current accrued, used, and available days per employee per type +Schedule coverage | Shifts per location per day — identifies understaffed days +Clock edit audit | All manager edits to time clock entries with reasons +Labor cost by department | Hours * pay_rate grouped by role (technician, instructor, staff) +Attendance | Scheduled vs actual clock-in times — identifies chronic lateness +Payroll export | CSV export formatted for common payroll services (QuickBooks Payroll, Gusto, ADP)