karaoke-gen 0.103.1__py3-none-any.whl → 0.107.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. backend/Dockerfile.base +1 -0
  2. backend/api/routes/admin.py +226 -3
  3. backend/api/routes/push.py +238 -0
  4. backend/api/routes/users.py +14 -3
  5. backend/config.py +12 -1
  6. backend/main.py +2 -1
  7. backend/models/job.py +4 -0
  8. backend/models/user.py +20 -2
  9. backend/services/encoding_interface.py +4 -0
  10. backend/services/gce_encoding/main.py +22 -8
  11. backend/services/job_manager.py +68 -11
  12. backend/services/job_notification_service.py +4 -21
  13. backend/services/push_notification_service.py +409 -0
  14. backend/services/stripe_service.py +2 -2
  15. backend/tests/conftest.py +2 -1
  16. backend/tests/test_admin_delete_outputs.py +352 -0
  17. backend/tests/test_gce_encoding_worker.py +229 -0
  18. backend/tests/test_impersonation.py +18 -3
  19. backend/tests/test_job_notification_service.py +24 -58
  20. backend/tests/test_push_notification_service.py +460 -0
  21. backend/tests/test_push_routes.py +357 -0
  22. backend/tests/test_stripe_service.py +205 -0
  23. backend/tests/test_video_worker_orchestrator.py +189 -0
  24. backend/workers/video_worker_orchestrator.py +23 -0
  25. karaoke_gen/instrumental_review/server.py +145 -35
  26. karaoke_gen/nextjs_frontend/__init__.py +98 -0
  27. karaoke_gen/nextjs_frontend/out/404/index.html +1 -0
  28. karaoke_gen/nextjs_frontend/out/404.html +1 -0
  29. karaoke_gen/nextjs_frontend/out/__next.__PAGE__.txt +9 -0
  30. karaoke_gen/nextjs_frontend/out/__next._full.txt +22 -0
  31. karaoke_gen/nextjs_frontend/out/__next._head.txt +8 -0
  32. karaoke_gen/nextjs_frontend/out/__next._index.txt +9 -0
  33. karaoke_gen/nextjs_frontend/out/__next._tree.txt +2 -0
  34. karaoke_gen/nextjs_frontend/out/_next/static/chunks/01a7f8fe40f1ff47.js +1 -0
  35. karaoke_gen/nextjs_frontend/out/_next/static/chunks/112f346e31f991df.js +4 -0
  36. karaoke_gen/nextjs_frontend/out/_next/static/chunks/16d1a4dd9d8a873a.js +3 -0
  37. karaoke_gen/nextjs_frontend/out/_next/static/chunks/1ab85c362b8b0e86.js +9 -0
  38. karaoke_gen/nextjs_frontend/out/_next/static/chunks/247eb132b7f7b574.js +1 -0
  39. karaoke_gen/nextjs_frontend/out/_next/static/chunks/2b80d15cc95e4818.js +1 -0
  40. karaoke_gen/nextjs_frontend/out/_next/static/chunks/32c7eba5cd46c1bc.js +7 -0
  41. karaoke_gen/nextjs_frontend/out/_next/static/chunks/483f26794eae53d0.js +1 -0
  42. karaoke_gen/nextjs_frontend/out/_next/static/chunks/550c3b02e85f196a.js +1 -0
  43. karaoke_gen/nextjs_frontend/out/_next/static/chunks/55c5ade44387bef8.js +1 -0
  44. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5628d92b5893add2.css +1 -0
  45. karaoke_gen/nextjs_frontend/out/_next/static/chunks/56ebf7665e4341c8.js +7 -0
  46. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5997132b61dec430.js +1 -0
  47. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5ea55255bce3eb9e.js +5 -0
  48. karaoke_gen/nextjs_frontend/out/_next/static/chunks/5eda89a57490b3cd.js +1 -0
  49. karaoke_gen/nextjs_frontend/out/_next/static/chunks/692f5d9e0d700c76.js +3 -0
  50. karaoke_gen/nextjs_frontend/out/_next/static/chunks/71d7a05b14f9f0f4.js +1 -0
  51. karaoke_gen/nextjs_frontend/out/_next/static/chunks/81ac355749ef3302.js +1 -0
  52. karaoke_gen/nextjs_frontend/out/_next/static/chunks/95f7e5934dbb0e5d.js +1 -0
  53. karaoke_gen/nextjs_frontend/out/_next/static/chunks/9bce8f19eaa46940.js +1 -0
  54. karaoke_gen/nextjs_frontend/out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  55. karaoke_gen/nextjs_frontend/out/_next/static/chunks/a9ed54eed3e14c92.js +2 -0
  56. karaoke_gen/nextjs_frontend/out/_next/static/chunks/b35cd41238ecfb17.js +1 -0
  57. karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5bc3c3d5ebd49eb.js +1 -0
  58. karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5c078c08db5ae32.js +5 -0
  59. karaoke_gen/nextjs_frontend/out/_next/static/chunks/be9c44a178104187.js +1 -0
  60. karaoke_gen/nextjs_frontend/out/_next/static/chunks/c4c840e18cb4861c.js +1 -0
  61. karaoke_gen/nextjs_frontend/out/_next/static/chunks/c645af7d6b65f73e.js +1 -0
  62. karaoke_gen/nextjs_frontend/out/_next/static/chunks/d2c5e2575df784d4.js +1 -0
  63. karaoke_gen/nextjs_frontend/out/_next/static/chunks/d30af02b96d81462.js +1 -0
  64. karaoke_gen/nextjs_frontend/out/_next/static/chunks/d9bdf64f4ec1e9b7.js +7 -0
  65. karaoke_gen/nextjs_frontend/out/_next/static/chunks/dcde6ed684dacd0e.js +5 -0
  66. karaoke_gen/nextjs_frontend/out/_next/static/chunks/e422cbe931246000.js +1 -0
  67. karaoke_gen/nextjs_frontend/out/_next/static/chunks/e483af34fc792d38.js +1 -0
  68. karaoke_gen/nextjs_frontend/out/_next/static/chunks/e57422aad6b897da.js +1 -0
  69. karaoke_gen/nextjs_frontend/out/_next/static/chunks/ef02697fb404726a.js +1 -0
  70. karaoke_gen/nextjs_frontend/out/_next/static/chunks/ff1a16fafef87110.js +1 -0
  71. karaoke_gen/nextjs_frontend/out/_next/static/chunks/turbopack-2d9ca3017a9deedf.js +3 -0
  72. karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_buildManifest.js +11 -0
  73. karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_clientMiddlewareManifest.json +1 -0
  74. karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_ssgManifest.js +1 -0
  75. karaoke_gen/nextjs_frontend/out/_not-found/__next._full.txt +18 -0
  76. karaoke_gen/nextjs_frontend/out/_not-found/__next._head.txt +8 -0
  77. karaoke_gen/nextjs_frontend/out/_not-found/__next._index.txt +9 -0
  78. karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.__PAGE__.txt +5 -0
  79. karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.txt +4 -0
  80. karaoke_gen/nextjs_frontend/out/_not-found/__next._tree.txt +2 -0
  81. karaoke_gen/nextjs_frontend/out/_not-found/index.html +1 -0
  82. karaoke_gen/nextjs_frontend/out/_not-found/index.txt +18 -0
  83. karaoke_gen/nextjs_frontend/out/admin/__next._full.txt +25 -0
  84. karaoke_gen/nextjs_frontend/out/admin/__next._head.txt +8 -0
  85. karaoke_gen/nextjs_frontend/out/admin/__next._index.txt +9 -0
  86. karaoke_gen/nextjs_frontend/out/admin/__next._tree.txt +2 -0
  87. karaoke_gen/nextjs_frontend/out/admin/__next.admin.__PAGE__.txt +9 -0
  88. karaoke_gen/nextjs_frontend/out/admin/__next.admin.txt +7 -0
  89. karaoke_gen/nextjs_frontend/out/admin/beta/__next._full.txt +25 -0
  90. karaoke_gen/nextjs_frontend/out/admin/beta/__next._head.txt +8 -0
  91. karaoke_gen/nextjs_frontend/out/admin/beta/__next._index.txt +9 -0
  92. karaoke_gen/nextjs_frontend/out/admin/beta/__next._tree.txt +2 -0
  93. karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.__PAGE__.txt +9 -0
  94. karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.txt +4 -0
  95. karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.txt +7 -0
  96. karaoke_gen/nextjs_frontend/out/admin/beta/index.html +1 -0
  97. karaoke_gen/nextjs_frontend/out/admin/beta/index.txt +25 -0
  98. karaoke_gen/nextjs_frontend/out/admin/index.html +1 -0
  99. karaoke_gen/nextjs_frontend/out/admin/index.txt +25 -0
  100. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._full.txt +25 -0
  101. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._head.txt +8 -0
  102. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._index.txt +9 -0
  103. karaoke_gen/nextjs_frontend/out/admin/jobs/__next._tree.txt +2 -0
  104. karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.__PAGE__.txt +9 -0
  105. karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.txt +4 -0
  106. karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.txt +7 -0
  107. karaoke_gen/nextjs_frontend/out/admin/jobs/index.html +1 -0
  108. karaoke_gen/nextjs_frontend/out/admin/jobs/index.txt +25 -0
  109. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._full.txt +25 -0
  110. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._head.txt +8 -0
  111. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._index.txt +9 -0
  112. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._tree.txt +2 -0
  113. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.__PAGE__.txt +9 -0
  114. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.txt +4 -0
  115. karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.txt +7 -0
  116. karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.html +1 -0
  117. karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.txt +25 -0
  118. karaoke_gen/nextjs_frontend/out/admin/searches/__next._full.txt +25 -0
  119. karaoke_gen/nextjs_frontend/out/admin/searches/__next._head.txt +8 -0
  120. karaoke_gen/nextjs_frontend/out/admin/searches/__next._index.txt +9 -0
  121. karaoke_gen/nextjs_frontend/out/admin/searches/__next._tree.txt +2 -0
  122. karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.__PAGE__.txt +9 -0
  123. karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.txt +4 -0
  124. karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.txt +7 -0
  125. karaoke_gen/nextjs_frontend/out/admin/searches/index.html +1 -0
  126. karaoke_gen/nextjs_frontend/out/admin/searches/index.txt +25 -0
  127. karaoke_gen/nextjs_frontend/out/admin/users/__next._full.txt +25 -0
  128. karaoke_gen/nextjs_frontend/out/admin/users/__next._head.txt +8 -0
  129. karaoke_gen/nextjs_frontend/out/admin/users/__next._index.txt +9 -0
  130. karaoke_gen/nextjs_frontend/out/admin/users/__next._tree.txt +2 -0
  131. karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.txt +7 -0
  132. karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.__PAGE__.txt +9 -0
  133. karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.txt +4 -0
  134. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._full.txt +25 -0
  135. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._head.txt +8 -0
  136. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._index.txt +9 -0
  137. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._tree.txt +2 -0
  138. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.txt +7 -0
  139. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.__PAGE__.txt +9 -0
  140. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.txt +4 -0
  141. karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.txt +4 -0
  142. karaoke_gen/nextjs_frontend/out/admin/users/detail/index.html +1 -0
  143. karaoke_gen/nextjs_frontend/out/admin/users/detail/index.txt +25 -0
  144. karaoke_gen/nextjs_frontend/out/admin/users/index.html +1 -0
  145. karaoke_gen/nextjs_frontend/out/admin/users/index.txt +25 -0
  146. karaoke_gen/nextjs_frontend/out/app/__next._full.txt +22 -0
  147. karaoke_gen/nextjs_frontend/out/app/__next._head.txt +8 -0
  148. karaoke_gen/nextjs_frontend/out/app/__next._index.txt +9 -0
  149. karaoke_gen/nextjs_frontend/out/app/__next._tree.txt +2 -0
  150. karaoke_gen/nextjs_frontend/out/app/__next.app.__PAGE__.txt +9 -0
  151. karaoke_gen/nextjs_frontend/out/app/__next.app.txt +4 -0
  152. karaoke_gen/nextjs_frontend/out/app/index.html +1 -0
  153. karaoke_gen/nextjs_frontend/out/app/index.txt +22 -0
  154. karaoke_gen/nextjs_frontend/out/app/jobs/__next._full.txt +19 -0
  155. karaoke_gen/nextjs_frontend/out/app/jobs/__next._head.txt +8 -0
  156. karaoke_gen/nextjs_frontend/out/app/jobs/__next._index.txt +9 -0
  157. karaoke_gen/nextjs_frontend/out/app/jobs/__next._tree.txt +2 -0
  158. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
  159. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.txt +4 -0
  160. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.txt +4 -0
  161. karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.txt +4 -0
  162. karaoke_gen/nextjs_frontend/out/app/jobs/index.html +1 -0
  163. karaoke_gen/nextjs_frontend/out/app/jobs/index.txt +19 -0
  164. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._full.txt +19 -0
  165. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._head.txt +8 -0
  166. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._index.txt +9 -0
  167. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._tree.txt +2 -0
  168. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
  169. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.txt +4 -0
  170. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.txt +4 -0
  171. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.txt +4 -0
  172. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.html +1 -0
  173. karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.txt +19 -0
  174. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._full.txt +19 -0
  175. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._head.txt +8 -0
  176. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._index.txt +9 -0
  177. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._tree.txt +2 -0
  178. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
  179. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.txt +4 -0
  180. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.txt +4 -0
  181. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.txt +4 -0
  182. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.html +1 -0
  183. karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.txt +19 -0
  184. karaoke_gen/nextjs_frontend/out/auth/verify/__next._full.txt +22 -0
  185. karaoke_gen/nextjs_frontend/out/auth/verify/__next._head.txt +8 -0
  186. karaoke_gen/nextjs_frontend/out/auth/verify/__next._index.txt +9 -0
  187. karaoke_gen/nextjs_frontend/out/auth/verify/__next._tree.txt +2 -0
  188. karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.txt +4 -0
  189. karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.__PAGE__.txt +9 -0
  190. karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.txt +4 -0
  191. karaoke_gen/nextjs_frontend/out/auth/verify/index.html +1 -0
  192. karaoke_gen/nextjs_frontend/out/auth/verify/index.txt +22 -0
  193. karaoke_gen/nextjs_frontend/out/index.html +1 -0
  194. karaoke_gen/nextjs_frontend/out/index.txt +22 -0
  195. karaoke_gen/nextjs_frontend/out/manifest.webmanifest +31 -0
  196. karaoke_gen/nextjs_frontend/out/order/success/__next._full.txt +22 -0
  197. karaoke_gen/nextjs_frontend/out/order/success/__next._head.txt +8 -0
  198. karaoke_gen/nextjs_frontend/out/order/success/__next._index.txt +9 -0
  199. karaoke_gen/nextjs_frontend/out/order/success/__next._tree.txt +2 -0
  200. karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.__PAGE__.txt +9 -0
  201. karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.txt +4 -0
  202. karaoke_gen/nextjs_frontend/out/order/success/__next.order.txt +4 -0
  203. karaoke_gen/nextjs_frontend/out/order/success/index.html +1 -0
  204. karaoke_gen/nextjs_frontend/out/order/success/index.txt +22 -0
  205. karaoke_gen/nextjs_frontend/out/payment/success/__next._full.txt +22 -0
  206. karaoke_gen/nextjs_frontend/out/payment/success/__next._head.txt +8 -0
  207. karaoke_gen/nextjs_frontend/out/payment/success/__next._index.txt +9 -0
  208. karaoke_gen/nextjs_frontend/out/payment/success/__next._tree.txt +2 -0
  209. karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.__PAGE__.txt +9 -0
  210. karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.txt +4 -0
  211. karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.txt +4 -0
  212. karaoke_gen/nextjs_frontend/out/payment/success/index.html +1 -0
  213. karaoke_gen/nextjs_frontend/out/payment/success/index.txt +22 -0
  214. karaoke_gen/nextjs_frontend/out/screenshots/email-action_reminder.png +0 -0
  215. karaoke_gen/nextjs_frontend/out/screenshots/email-beta_welcome.png +0 -0
  216. karaoke_gen/nextjs_frontend/out/screenshots/email-job_completion.png +0 -0
  217. karaoke_gen/nextjs_frontend/out/screenshots/example-output.avif +0 -0
  218. karaoke_gen/nextjs_frontend/out/screenshots/homepage-full.png +0 -0
  219. karaoke_gen/nextjs_frontend/out/screenshots/homepage-hero.png +0 -0
  220. karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.avif +0 -0
  221. karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.png +0 -0
  222. karaoke_gen/nextjs_frontend/out/screenshots/job-dashboard.avif +0 -0
  223. karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.avif +0 -0
  224. karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.png +0 -0
  225. karaoke_gen/nextjs_frontend/out/sw.js +183 -0
  226. karaoke_gen/utils/cli_args.py +3 -3
  227. karaoke_gen/utils/gen_cli.py +4 -0
  228. karaoke_gen/utils/remote_cli.py +8 -40
  229. {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/METADATA +2 -1
  230. {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/RECORD +244 -131
  231. {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/WHEEL +1 -1
  232. lyrics_transcriber/correction/agentic/agent.py +83 -60
  233. lyrics_transcriber/correction/anchor_sequence.py +48 -3
  234. lyrics_transcriber/correction/corrector.py +92 -58
  235. lyrics_transcriber/review/server.py +165 -33
  236. lyrics_transcriber/utils/tracing.py +214 -0
  237. karaoke_gen/instrumental_review/static/index.html +0 -1695
  238. lyrics_transcriber/frontend/.gitignore +0 -24
  239. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +0 -935
  240. lyrics_transcriber/frontend/.yarnrc.yml +0 -3
  241. lyrics_transcriber/frontend/README.md +0 -50
  242. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +0 -210
  243. lyrics_transcriber/frontend/__init__.py +0 -25
  244. lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +0 -207
  245. lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +0 -226
  246. lyrics_transcriber/frontend/eslint.config.js +0 -28
  247. lyrics_transcriber/frontend/index.html +0 -22
  248. lyrics_transcriber/frontend/package-lock.json +0 -4553
  249. lyrics_transcriber/frontend/package.json +0 -48
  250. lyrics_transcriber/frontend/playwright.config.ts +0 -69
  251. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  252. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  253. lyrics_transcriber/frontend/src/App.tsx +0 -243
  254. lyrics_transcriber/frontend/src/api.ts +0 -262
  255. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +0 -111
  256. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +0 -114
  257. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +0 -204
  258. lyrics_transcriber/frontend/src/components/AppHeader.tsx +0 -65
  259. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +0 -180
  260. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +0 -175
  261. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +0 -359
  262. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +0 -281
  263. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +0 -162
  264. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +0 -257
  265. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +0 -94
  266. lyrics_transcriber/frontend/src/components/EditModal.tsx +0 -720
  267. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +0 -592
  268. lyrics_transcriber/frontend/src/components/EditWordList.tsx +0 -431
  269. lyrics_transcriber/frontend/src/components/FileUpload.tsx +0 -77
  270. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +0 -467
  271. lyrics_transcriber/frontend/src/components/Header.tsx +0 -520
  272. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +0 -1526
  273. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +0 -216
  274. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +0 -721
  275. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +0 -80
  276. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +0 -999
  277. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +0 -51
  278. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +0 -127
  279. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +0 -67
  280. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +0 -23
  281. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +0 -177
  282. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +0 -268
  283. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +0 -336
  284. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +0 -354
  285. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +0 -64
  286. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +0 -383
  287. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +0 -131
  288. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +0 -266
  289. lyrics_transcriber/frontend/src/components/WordDivider.tsx +0 -191
  290. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +0 -466
  291. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +0 -56
  292. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +0 -89
  293. lyrics_transcriber/frontend/src/components/shared/constants.ts +0 -30
  294. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +0 -180
  295. lyrics_transcriber/frontend/src/components/shared/styles.ts +0 -13
  296. lyrics_transcriber/frontend/src/components/shared/types.js +0 -2
  297. lyrics_transcriber/frontend/src/components/shared/types.ts +0 -135
  298. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +0 -177
  299. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +0 -78
  300. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +0 -75
  301. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +0 -360
  302. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +0 -110
  303. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +0 -22
  304. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +0 -537
  305. lyrics_transcriber/frontend/src/main.tsx +0 -11
  306. lyrics_transcriber/frontend/src/theme.ts +0 -406
  307. lyrics_transcriber/frontend/src/types/global.d.ts +0 -9
  308. lyrics_transcriber/frontend/src/types.js +0 -2
  309. lyrics_transcriber/frontend/src/types.ts +0 -199
  310. lyrics_transcriber/frontend/src/validation.ts +0 -132
  311. lyrics_transcriber/frontend/src/vite-env.d.ts +0 -1
  312. lyrics_transcriber/frontend/tsconfig.app.json +0 -26
  313. lyrics_transcriber/frontend/tsconfig.json +0 -25
  314. lyrics_transcriber/frontend/tsconfig.node.json +0 -23
  315. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +0 -1
  316. lyrics_transcriber/frontend/update_version.js +0 -11
  317. lyrics_transcriber/frontend/vite.config.d.ts +0 -2
  318. lyrics_transcriber/frontend/vite.config.js +0 -15
  319. lyrics_transcriber/frontend/vite.config.ts +0 -16
  320. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  321. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  322. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  323. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js +0 -44465
  324. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +0 -1
  325. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  326. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  327. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  328. lyrics_transcriber/frontend/web_assets/index.html +0 -22
  329. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  330. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +0 -5
  331. lyrics_transcriber/frontend/yarn.lock +0 -3711
  332. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/apple-touch-icon.png +0 -0
  333. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-16x16.png +0 -0
  334. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-32x32.png +0 -0
  335. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon.ico +0 -0
  336. {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/nomad-karaoke-logo.svg +0 -0
  337. /lyrics_transcriber/frontend/public/nomad-karaoke-logo.png → /karaoke_gen/nextjs_frontend/out/nomad-logo.png +0 -0
  338. {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/entry_points.txt +0 -0
  339. {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,357 @@
1
+ """
2
+ Tests for Push Notification API routes.
3
+
4
+ Tests the /api/push/* endpoints for subscription management.
5
+ """
6
+ import pytest
7
+ from unittest.mock import Mock, patch, AsyncMock
8
+ from fastapi.testclient import TestClient
9
+ from dataclasses import dataclass
10
+ from typing import Optional
11
+
12
+ from backend.main import app
13
+ from backend.api.dependencies import require_auth, require_admin
14
+ from backend.services.auth_service import UserType, AuthResult
15
+
16
+
17
+ @pytest.fixture
18
+ def client():
19
+ """Create test client."""
20
+ return TestClient(app)
21
+
22
+
23
+ @pytest.fixture
24
+ def mock_auth_result():
25
+ """Create a mock AuthResult for regular user."""
26
+ return AuthResult(
27
+ is_valid=True,
28
+ user_type=UserType.UNLIMITED,
29
+ remaining_uses=-1,
30
+ message="Valid",
31
+ user_email="test@example.com",
32
+ is_admin=False,
33
+ )
34
+
35
+
36
+ @pytest.fixture
37
+ def mock_admin_auth_result():
38
+ """Create a mock AuthResult for admin user."""
39
+ return AuthResult(
40
+ is_valid=True,
41
+ user_type=UserType.ADMIN,
42
+ remaining_uses=-1,
43
+ message="Valid",
44
+ user_email="admin@nomadkaraoke.com",
45
+ is_admin=True,
46
+ )
47
+
48
+
49
+ class TestGetVapidPublicKey:
50
+ """Tests for GET /api/push/vapid-public-key."""
51
+
52
+ def test_returns_disabled_when_feature_off(self, client):
53
+ """Returns enabled=false when push notifications disabled."""
54
+ with patch('backend.api.routes.push.get_settings') as mock_settings:
55
+ mock_settings.return_value.enable_push_notifications = False
56
+
57
+ response = client.get("/api/push/vapid-public-key")
58
+
59
+ assert response.status_code == 200
60
+ data = response.json()
61
+ assert data["enabled"] is False
62
+ assert data["vapid_public_key"] is None
63
+
64
+ def test_returns_key_when_enabled(self, client):
65
+ """Returns public key when push notifications enabled."""
66
+ with patch('backend.api.routes.push.get_settings') as mock_settings, \
67
+ patch('backend.api.routes.push.get_push_notification_service') as mock_service:
68
+ mock_settings.return_value.enable_push_notifications = True
69
+ mock_service.return_value.get_public_key.return_value = "test-public-key-123"
70
+
71
+ response = client.get("/api/push/vapid-public-key")
72
+
73
+ assert response.status_code == 200
74
+ data = response.json()
75
+ assert data["enabled"] is True
76
+ assert data["vapid_public_key"] == "test-public-key-123"
77
+
78
+
79
+ class TestSubscribe:
80
+ """Tests for POST /api/push/subscribe."""
81
+
82
+ def test_requires_authentication(self, client):
83
+ """Returns 401 when not authenticated."""
84
+ # Clear any auth overrides to test real auth
85
+ app.dependency_overrides.clear()
86
+
87
+ response = client.post(
88
+ "/api/push/subscribe",
89
+ json={
90
+ "endpoint": "https://push.example.com/endpoint",
91
+ "keys": {"p256dh": "key1", "auth": "key2"}
92
+ }
93
+ )
94
+
95
+ assert response.status_code == 401
96
+
97
+ def test_returns_503_when_disabled(self, client, mock_auth_result):
98
+ """Returns 503 when push notifications disabled."""
99
+ async def override_auth():
100
+ return mock_auth_result
101
+
102
+ app.dependency_overrides[require_auth] = override_auth
103
+
104
+ try:
105
+ with patch('backend.api.routes.push.get_settings') as mock_settings:
106
+ mock_settings.return_value.enable_push_notifications = False
107
+
108
+ response = client.post(
109
+ "/api/push/subscribe",
110
+ json={
111
+ "endpoint": "https://push.example.com/endpoint",
112
+ "keys": {"p256dh": "key1", "auth": "key2"}
113
+ }
114
+ )
115
+
116
+ assert response.status_code == 503
117
+ assert "not enabled" in response.json()["detail"].lower()
118
+ finally:
119
+ app.dependency_overrides.clear()
120
+
121
+ def test_validates_required_keys(self, client, mock_auth_result):
122
+ """Returns 400 when keys missing."""
123
+ async def override_auth():
124
+ return mock_auth_result
125
+
126
+ app.dependency_overrides[require_auth] = override_auth
127
+
128
+ try:
129
+ with patch('backend.api.routes.push.get_settings') as mock_settings:
130
+ mock_settings.return_value.enable_push_notifications = True
131
+
132
+ response = client.post(
133
+ "/api/push/subscribe",
134
+ json={
135
+ "endpoint": "https://push.example.com/endpoint",
136
+ "keys": {"p256dh": "key1"} # Missing auth
137
+ }
138
+ )
139
+
140
+ assert response.status_code == 400
141
+ assert "missing" in response.json()["detail"].lower()
142
+ finally:
143
+ app.dependency_overrides.clear()
144
+
145
+ def test_successful_subscription(self, client, mock_auth_result):
146
+ """Successfully subscribes user to push notifications."""
147
+ async def override_auth():
148
+ return mock_auth_result
149
+
150
+ app.dependency_overrides[require_auth] = override_auth
151
+
152
+ try:
153
+ with patch('backend.api.routes.push.get_settings') as mock_settings, \
154
+ patch('backend.api.routes.push.get_push_notification_service') as mock_service:
155
+ mock_settings.return_value.enable_push_notifications = True
156
+ mock_service.return_value.add_subscription = AsyncMock(return_value=True)
157
+
158
+ response = client.post(
159
+ "/api/push/subscribe",
160
+ json={
161
+ "endpoint": "https://push.example.com/endpoint",
162
+ "keys": {"p256dh": "key1", "auth": "key2"},
163
+ "device_name": "Test Device"
164
+ }
165
+ )
166
+
167
+ assert response.status_code == 200
168
+ data = response.json()
169
+ assert data["status"] == "success"
170
+ mock_service.return_value.add_subscription.assert_called_once_with(
171
+ user_email="test@example.com",
172
+ endpoint="https://push.example.com/endpoint",
173
+ keys={"p256dh": "key1", "auth": "key2"},
174
+ device_name="Test Device"
175
+ )
176
+ finally:
177
+ app.dependency_overrides.clear()
178
+
179
+
180
+ class TestUnsubscribe:
181
+ """Tests for POST /api/push/unsubscribe."""
182
+
183
+ def test_requires_authentication(self, client):
184
+ """Returns 401 when not authenticated."""
185
+ app.dependency_overrides.clear()
186
+
187
+ response = client.post(
188
+ "/api/push/unsubscribe",
189
+ json={"endpoint": "https://push.example.com/endpoint"}
190
+ )
191
+
192
+ assert response.status_code == 401
193
+
194
+ def test_successful_unsubscribe(self, client, mock_auth_result):
195
+ """Successfully unsubscribes from push notifications."""
196
+ async def override_auth():
197
+ return mock_auth_result
198
+
199
+ app.dependency_overrides[require_auth] = override_auth
200
+
201
+ try:
202
+ with patch('backend.api.routes.push.get_push_notification_service') as mock_service:
203
+ mock_service.return_value.remove_subscription = AsyncMock(return_value=True)
204
+
205
+ response = client.post(
206
+ "/api/push/unsubscribe",
207
+ json={"endpoint": "https://push.example.com/endpoint"}
208
+ )
209
+
210
+ assert response.status_code == 200
211
+ data = response.json()
212
+ assert data["status"] == "success"
213
+ mock_service.return_value.remove_subscription.assert_called_once()
214
+ finally:
215
+ app.dependency_overrides.clear()
216
+
217
+ def test_unsubscribe_not_found_succeeds(self, client, mock_auth_result):
218
+ """Returns success even when subscription not found."""
219
+ async def override_auth():
220
+ return mock_auth_result
221
+
222
+ app.dependency_overrides[require_auth] = override_auth
223
+
224
+ try:
225
+ with patch('backend.api.routes.push.get_push_notification_service') as mock_service:
226
+ mock_service.return_value.remove_subscription = AsyncMock(return_value=False)
227
+
228
+ response = client.post(
229
+ "/api/push/unsubscribe",
230
+ json={"endpoint": "https://push.example.com/unknown"}
231
+ )
232
+
233
+ assert response.status_code == 200
234
+ data = response.json()
235
+ assert data["status"] == "success"
236
+ finally:
237
+ app.dependency_overrides.clear()
238
+
239
+
240
+ class TestListSubscriptions:
241
+ """Tests for GET /api/push/subscriptions."""
242
+
243
+ def test_requires_authentication(self, client):
244
+ """Returns 401 when not authenticated."""
245
+ app.dependency_overrides.clear()
246
+
247
+ response = client.get("/api/push/subscriptions")
248
+ assert response.status_code == 401
249
+
250
+ def test_returns_user_subscriptions(self, client, mock_auth_result):
251
+ """Returns list of user's subscriptions."""
252
+ async def override_auth():
253
+ return mock_auth_result
254
+
255
+ app.dependency_overrides[require_auth] = override_auth
256
+
257
+ try:
258
+ with patch('backend.api.routes.push.get_push_notification_service') as mock_service:
259
+ mock_service.return_value.list_subscriptions = AsyncMock(return_value=[
260
+ {
261
+ "endpoint": "https://push.example.com/endpoint1",
262
+ "device_name": "Device 1",
263
+ "created_at": "2024-01-01T00:00:00Z",
264
+ "last_used_at": None
265
+ },
266
+ {
267
+ "endpoint": "https://push.example.com/endpoint2",
268
+ "device_name": "Device 2",
269
+ "created_at": "2024-01-02T00:00:00Z",
270
+ "last_used_at": "2024-01-03T00:00:00Z"
271
+ }
272
+ ])
273
+
274
+ response = client.get("/api/push/subscriptions")
275
+
276
+ assert response.status_code == 200
277
+ data = response.json()
278
+ assert data["count"] == 2
279
+ assert len(data["subscriptions"]) == 2
280
+ assert data["subscriptions"][0]["device_name"] == "Device 1"
281
+ finally:
282
+ app.dependency_overrides.clear()
283
+
284
+
285
+ class TestTestNotification:
286
+ """Tests for POST /api/push/test."""
287
+
288
+ def test_requires_admin(self, client, mock_auth_result):
289
+ """Returns 403 when not admin."""
290
+ # Use a non-admin auth result
291
+ async def override_admin():
292
+ from fastapi import HTTPException
293
+ raise HTTPException(status_code=403, detail="Admin access required")
294
+
295
+ app.dependency_overrides[require_admin] = override_admin
296
+
297
+ try:
298
+ response = client.post(
299
+ "/api/push/test",
300
+ json={"title": "Test", "body": "Test message"}
301
+ )
302
+
303
+ assert response.status_code == 403
304
+ finally:
305
+ app.dependency_overrides.clear()
306
+
307
+ def test_returns_503_when_disabled(self, client, mock_admin_auth_result):
308
+ """Returns 503 when push notifications disabled."""
309
+ async def override_admin():
310
+ return mock_admin_auth_result
311
+
312
+ app.dependency_overrides[require_admin] = override_admin
313
+
314
+ try:
315
+ with patch('backend.api.routes.push.get_settings') as mock_settings:
316
+ mock_settings.return_value.enable_push_notifications = False
317
+
318
+ response = client.post(
319
+ "/api/push/test",
320
+ json={"title": "Test", "body": "Test message"}
321
+ )
322
+
323
+ assert response.status_code == 503
324
+ finally:
325
+ app.dependency_overrides.clear()
326
+
327
+ def test_sends_test_notification(self, client, mock_admin_auth_result):
328
+ """Successfully sends test notification to admin."""
329
+ async def override_admin():
330
+ return mock_admin_auth_result
331
+
332
+ app.dependency_overrides[require_admin] = override_admin
333
+
334
+ try:
335
+ with patch('backend.api.routes.push.get_settings') as mock_settings, \
336
+ patch('backend.api.routes.push.get_push_notification_service') as mock_service:
337
+ mock_settings.return_value.enable_push_notifications = True
338
+ mock_service.return_value.send_push = AsyncMock(return_value=2)
339
+
340
+ response = client.post(
341
+ "/api/push/test",
342
+ json={"title": "Test Title", "body": "Test Body"}
343
+ )
344
+
345
+ assert response.status_code == 200
346
+ data = response.json()
347
+ assert data["status"] == "success"
348
+ assert data["sent_count"] == 2
349
+ mock_service.return_value.send_push.assert_called_once_with(
350
+ user_email="admin@nomadkaraoke.com",
351
+ title="Test Title",
352
+ body="Test Body",
353
+ url="/app/",
354
+ tag="test"
355
+ )
356
+ finally:
357
+ app.dependency_overrides.clear()
@@ -0,0 +1,205 @@
1
+ """
2
+ Tests for StripeService.
3
+
4
+ Tests cover:
5
+ - Made-for-you checkout session URL generation
6
+ - Credit purchase checkout session URL generation
7
+ - Package validation
8
+ """
9
+
10
+ import pytest
11
+ from unittest.mock import Mock, patch, MagicMock
12
+ import os
13
+
14
+
15
+ class TestStripeServiceUrls:
16
+ """Tests for checkout session URL generation."""
17
+
18
+ @pytest.fixture
19
+ def stripe_service(self):
20
+ """Create a StripeService instance with mocked Stripe."""
21
+ # Set required env vars
22
+ with patch.dict(os.environ, {
23
+ 'STRIPE_SECRET_KEY': 'sk_test_fake',
24
+ 'FRONTEND_URL': 'https://gen.nomadkaraoke.com',
25
+ }):
26
+ # Import here to pick up env vars
27
+ from backend.services.stripe_service import StripeService
28
+ service = StripeService()
29
+ return service
30
+
31
+ def test_made_for_you_success_url_uses_frontend_url(self, stripe_service):
32
+ """Test that made-for-you checkout uses frontend_url, not hardcoded domain."""
33
+ # Mock stripe.checkout.Session.create to capture the params
34
+ with patch('stripe.checkout.Session.create') as mock_create:
35
+ mock_session = MagicMock()
36
+ mock_session.id = 'cs_test_123'
37
+ mock_session.url = 'https://checkout.stripe.com/test'
38
+ mock_create.return_value = mock_session
39
+
40
+ success, url, message = stripe_service.create_made_for_you_checkout_session(
41
+ customer_email='customer@example.com',
42
+ artist='Test Artist',
43
+ title='Test Song',
44
+ )
45
+
46
+ # Verify the call was made
47
+ assert mock_create.called
48
+ call_kwargs = mock_create.call_args[1]
49
+
50
+ # Verify success_url uses frontend_url and correct path
51
+ success_url = call_kwargs['success_url']
52
+ assert success_url.startswith('https://gen.nomadkaraoke.com')
53
+ assert '/order/success' in success_url
54
+ assert 'session_id=' in success_url
55
+
56
+ def test_made_for_you_success_url_not_hardcoded_to_marketing_site(self, stripe_service):
57
+ """Test that success_url is NOT the old hardcoded marketing site URL."""
58
+ with patch('stripe.checkout.Session.create') as mock_create:
59
+ mock_session = MagicMock()
60
+ mock_session.id = 'cs_test_123'
61
+ mock_session.url = 'https://checkout.stripe.com/test'
62
+ mock_create.return_value = mock_session
63
+
64
+ stripe_service.create_made_for_you_checkout_session(
65
+ customer_email='customer@example.com',
66
+ artist='Test Artist',
67
+ title='Test Song',
68
+ )
69
+
70
+ call_kwargs = mock_create.call_args[1]
71
+ success_url = call_kwargs['success_url']
72
+
73
+ # The bug was: success_url = "https://nomadkaraoke.com/order/success/"
74
+ # (note: nomadkaraoke.com WITHOUT the 'gen.' prefix - that's the marketing site)
75
+ # This should NOT be the case anymore - should use gen.nomadkaraoke.com
76
+ assert not success_url.startswith('https://nomadkaraoke.com/')
77
+ # Should use the frontend URL (gen.nomadkaraoke.com)
78
+ assert success_url.startswith('https://gen.nomadkaraoke.com')
79
+
80
+ def test_made_for_you_respects_custom_frontend_url(self):
81
+ """Test that made-for-you checkout uses custom FRONTEND_URL if set."""
82
+ with patch.dict(os.environ, {
83
+ 'STRIPE_SECRET_KEY': 'sk_test_fake',
84
+ 'FRONTEND_URL': 'https://custom.example.com',
85
+ }):
86
+ from backend.services.stripe_service import StripeService
87
+ service = StripeService()
88
+
89
+ with patch('stripe.checkout.Session.create') as mock_create:
90
+ mock_session = MagicMock()
91
+ mock_session.id = 'cs_test_123'
92
+ mock_session.url = 'https://checkout.stripe.com/test'
93
+ mock_create.return_value = mock_session
94
+
95
+ service.create_made_for_you_checkout_session(
96
+ customer_email='customer@example.com',
97
+ artist='Test Artist',
98
+ title='Test Song',
99
+ )
100
+
101
+ call_kwargs = mock_create.call_args[1]
102
+ success_url = call_kwargs['success_url']
103
+ assert success_url.startswith('https://custom.example.com')
104
+
105
+ def test_credit_purchase_success_url_uses_payment_success(self, stripe_service):
106
+ """Test that credit purchase checkout uses /payment/success path."""
107
+ with patch('stripe.checkout.Session.create') as mock_create:
108
+ mock_session = MagicMock()
109
+ mock_session.id = 'cs_test_123'
110
+ mock_session.url = 'https://checkout.stripe.com/test'
111
+ mock_create.return_value = mock_session
112
+
113
+ success, url, message = stripe_service.create_checkout_session(
114
+ package_id='1_credit',
115
+ user_email='user@example.com',
116
+ )
117
+
118
+ call_kwargs = mock_create.call_args[1]
119
+ success_url = call_kwargs['success_url']
120
+
121
+ # Credit purchases go to /payment/success
122
+ assert '/payment/success' in success_url
123
+ assert success_url.startswith('https://gen.nomadkaraoke.com')
124
+
125
+ def test_made_for_you_uses_order_success_path(self, stripe_service):
126
+ """Test that made-for-you uses /order/success path (not /payment/success)."""
127
+ with patch('stripe.checkout.Session.create') as mock_create:
128
+ mock_session = MagicMock()
129
+ mock_session.id = 'cs_test_123'
130
+ mock_session.url = 'https://checkout.stripe.com/test'
131
+ mock_create.return_value = mock_session
132
+
133
+ stripe_service.create_made_for_you_checkout_session(
134
+ customer_email='customer@example.com',
135
+ artist='Test Artist',
136
+ title='Test Song',
137
+ )
138
+
139
+ call_kwargs = mock_create.call_args[1]
140
+ success_url = call_kwargs['success_url']
141
+
142
+ # Made-for-you orders go to /order/success (different messaging)
143
+ assert '/order/success' in success_url
144
+ # Should NOT go to /payment/success
145
+ assert '/payment/success' not in success_url
146
+
147
+
148
+ class TestStripeServiceMetadata:
149
+ """Tests for checkout session metadata."""
150
+
151
+ @pytest.fixture
152
+ def stripe_service(self):
153
+ """Create a StripeService instance with mocked Stripe."""
154
+ with patch.dict(os.environ, {
155
+ 'STRIPE_SECRET_KEY': 'sk_test_fake',
156
+ 'FRONTEND_URL': 'https://gen.nomadkaraoke.com',
157
+ }):
158
+ from backend.services.stripe_service import StripeService
159
+ return StripeService()
160
+
161
+ def test_made_for_you_includes_order_type_metadata(self, stripe_service):
162
+ """Test that made-for-you checkout includes order_type in metadata."""
163
+ with patch('stripe.checkout.Session.create') as mock_create:
164
+ mock_session = MagicMock()
165
+ mock_session.id = 'cs_test_123'
166
+ mock_session.url = 'https://checkout.stripe.com/test'
167
+ mock_create.return_value = mock_session
168
+
169
+ stripe_service.create_made_for_you_checkout_session(
170
+ customer_email='customer@example.com',
171
+ artist='Test Artist',
172
+ title='Test Song',
173
+ notes='Special request',
174
+ )
175
+
176
+ call_kwargs = mock_create.call_args[1]
177
+ metadata = call_kwargs['metadata']
178
+
179
+ assert metadata['order_type'] == 'made_for_you'
180
+ assert metadata['customer_email'] == 'customer@example.com'
181
+ assert metadata['artist'] == 'Test Artist'
182
+ assert metadata['title'] == 'Test Song'
183
+ assert metadata['notes'] == 'Special request'
184
+
185
+ def test_made_for_you_truncates_long_notes(self, stripe_service):
186
+ """Test that notes longer than 500 chars are truncated."""
187
+ with patch('stripe.checkout.Session.create') as mock_create:
188
+ mock_session = MagicMock()
189
+ mock_session.id = 'cs_test_123'
190
+ mock_session.url = 'https://checkout.stripe.com/test'
191
+ mock_create.return_value = mock_session
192
+
193
+ long_notes = 'x' * 600
194
+
195
+ stripe_service.create_made_for_you_checkout_session(
196
+ customer_email='customer@example.com',
197
+ artist='Test Artist',
198
+ title='Test Song',
199
+ notes=long_notes,
200
+ )
201
+
202
+ call_kwargs = mock_create.call_args[1]
203
+ metadata = call_kwargs['metadata']
204
+
205
+ assert len(metadata['notes']) == 500