adserver-dashboard 1.0.0

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 (525) hide show
  1. package/.ci/staging.yml +191 -0
  2. package/.dockerignore +117 -0
  3. package/.env +40 -0
  4. package/.env.staging +38 -0
  5. package/.gitlab-ci.yml +16 -0
  6. package/DEMO_STATUS.md +579 -0
  7. package/Dockerfile +61 -0
  8. package/Influence-MW-AdServer-12-02-2026/client/index.html +17 -0
  9. package/Influence-MW-AdServer-12-02-2026/client/public/favicon.png +0 -0
  10. package/Influence-MW-AdServer-12-02-2026/client/src/App.tsx +91 -0
  11. package/Influence-MW-AdServer-12-02-2026/client/src/components/advanced-map-drawer.tsx +1131 -0
  12. package/Influence-MW-AdServer-12-02-2026/client/src/components/ai-recommendation-panel.tsx +379 -0
  13. package/Influence-MW-AdServer-12-02-2026/client/src/components/app-sidebar.tsx +183 -0
  14. package/Influence-MW-AdServer-12-02-2026/client/src/components/auto-optimize-button.tsx +184 -0
  15. package/Influence-MW-AdServer-12-02-2026/client/src/components/availability-drawer.tsx +385 -0
  16. package/Influence-MW-AdServer-12-02-2026/client/src/components/brand-insights-panel.tsx +87 -0
  17. package/Influence-MW-AdServer-12-02-2026/client/src/components/create-agency-drawer.tsx +198 -0
  18. package/Influence-MW-AdServer-12-02-2026/client/src/components/create-brand-drawer.tsx +275 -0
  19. package/Influence-MW-AdServer-12-02-2026/client/src/components/creative-assignment.tsx +526 -0
  20. package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table-toolbar.tsx +148 -0
  21. package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table.tsx +158 -0
  22. package/Influence-MW-AdServer-12-02-2026/client/src/components/filter-drawer.tsx +356 -0
  23. package/Influence-MW-AdServer-12-02-2026/client/src/components/form-insights-panel.tsx +82 -0
  24. package/Influence-MW-AdServer-12-02-2026/client/src/components/geography-selector.tsx +699 -0
  25. package/Influence-MW-AdServer-12-02-2026/client/src/components/header-user-menu.tsx +178 -0
  26. package/Influence-MW-AdServer-12-02-2026/client/src/components/history-drawer.tsx +313 -0
  27. package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-availability-section.tsx +176 -0
  28. package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-format-drawer.tsx +173 -0
  29. package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-selector.tsx +401 -0
  30. package/Influence-MW-AdServer-12-02-2026/client/src/components/manual-inventory-drawer.tsx +368 -0
  31. package/Influence-MW-AdServer-12-02-2026/client/src/components/mapbox-map.tsx +368 -0
  32. package/Influence-MW-AdServer-12-02-2026/client/src/components/market-insights-panel.tsx +202 -0
  33. package/Influence-MW-AdServer-12-02-2026/client/src/components/media-owner-drawer.tsx +217 -0
  34. package/Influence-MW-AdServer-12-02-2026/client/src/components/metric-card.tsx +58 -0
  35. package/Influence-MW-AdServer-12-02-2026/client/src/components/page-header.tsx +27 -0
  36. package/Influence-MW-AdServer-12-02-2026/client/src/components/player-status-indicator.tsx +137 -0
  37. package/Influence-MW-AdServer-12-02-2026/client/src/components/poi-targeting-drawer.tsx +298 -0
  38. package/Influence-MW-AdServer-12-02-2026/client/src/components/recommendation-score-badge.tsx +102 -0
  39. package/Influence-MW-AdServer-12-02-2026/client/src/components/recommended-inventories-panel.tsx +248 -0
  40. package/Influence-MW-AdServer-12-02-2026/client/src/components/searchable-combobox.tsx +134 -0
  41. package/Influence-MW-AdServer-12-02-2026/client/src/components/signal-visualizations.tsx +407 -0
  42. package/Influence-MW-AdServer-12-02-2026/client/src/components/status-badge.tsx +35 -0
  43. package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-provider.tsx +73 -0
  44. package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-toggle.tsx +37 -0
  45. package/Influence-MW-AdServer-12-02-2026/client/src/components/traffic-slider.tsx +75 -0
  46. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/accordion.tsx +56 -0
  47. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert-dialog.tsx +139 -0
  48. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert.tsx +59 -0
  49. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/aspect-ratio.tsx +5 -0
  50. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/avatar.tsx +51 -0
  51. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/badge.tsx +38 -0
  52. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/breadcrumb.tsx +115 -0
  53. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/button.tsx +62 -0
  54. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/calendar.tsx +68 -0
  55. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/card.tsx +85 -0
  56. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/carousel.tsx +260 -0
  57. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/chart.tsx +365 -0
  58. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/checkbox.tsx +28 -0
  59. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/collapsible.tsx +11 -0
  60. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/command.tsx +151 -0
  61. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/context-menu.tsx +198 -0
  62. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dialog.tsx +122 -0
  63. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/drawer.tsx +118 -0
  64. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dropdown-menu.tsx +198 -0
  65. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/form.tsx +178 -0
  66. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/hover-card.tsx +29 -0
  67. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input-otp.tsx +69 -0
  68. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input.tsx +23 -0
  69. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/label.tsx +24 -0
  70. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/menubar.tsx +256 -0
  71. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/navigation-menu.tsx +128 -0
  72. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/pagination.tsx +117 -0
  73. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/popover.tsx +29 -0
  74. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/progress.tsx +28 -0
  75. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/radio-group.tsx +42 -0
  76. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/resizable.tsx +45 -0
  77. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/scroll-area.tsx +46 -0
  78. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/select.tsx +160 -0
  79. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/separator.tsx +29 -0
  80. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sheet.tsx +140 -0
  81. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sidebar.tsx +727 -0
  82. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/skeleton.tsx +15 -0
  83. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/slider.tsx +26 -0
  84. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/switch.tsx +27 -0
  85. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/table.tsx +117 -0
  86. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tabs.tsx +53 -0
  87. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/textarea.tsx +22 -0
  88. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toast.tsx +127 -0
  89. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toaster.tsx +33 -0
  90. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle-group.tsx +61 -0
  91. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle.tsx +43 -0
  92. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tooltip.tsx +30 -0
  93. package/Influence-MW-AdServer-12-02-2026/client/src/components/vendor-stores-modal.tsx +336 -0
  94. package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-drawer.tsx +359 -0
  95. package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-selector.tsx +436 -0
  96. package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-mobile.tsx +19 -0
  97. package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-toast.ts +191 -0
  98. package/Influence-MW-AdServer-12-02-2026/client/src/index.css +244 -0
  99. package/Influence-MW-AdServer-12-02-2026/client/src/lib/queryClient.ts +57 -0
  100. package/Influence-MW-AdServer-12-02-2026/client/src/lib/utils.ts +39 -0
  101. package/Influence-MW-AdServer-12-02-2026/client/src/lib/venue-taxonomy.ts +532 -0
  102. package/Influence-MW-AdServer-12-02-2026/client/src/main.tsx +5 -0
  103. package/Influence-MW-AdServer-12-02-2026/client/src/pages/assign-creative.tsx +781 -0
  104. package/Influence-MW-AdServer-12-02-2026/client/src/pages/content-hub.tsx +995 -0
  105. package/Influence-MW-AdServer-12-02-2026/client/src/pages/custom-pois.tsx +431 -0
  106. package/Influence-MW-AdServer-12-02-2026/client/src/pages/dashboard.tsx +620 -0
  107. package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-detail.tsx +1062 -0
  108. package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-form.tsx +1570 -0
  109. package/Influence-MW-AdServer-12-02-2026/client/src/pages/deals.tsx +716 -0
  110. package/Influence-MW-AdServer-12-02-2026/client/src/pages/edit-creative-assignment.tsx +1051 -0
  111. package/Influence-MW-AdServer-12-02-2026/client/src/pages/geotargeting.tsx +675 -0
  112. package/Influence-MW-AdServer-12-02-2026/client/src/pages/integrations.tsx +425 -0
  113. package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-creatives.tsx +622 -0
  114. package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-form.tsx +3132 -0
  115. package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-items.tsx +530 -0
  116. package/Influence-MW-AdServer-12-02-2026/client/src/pages/not-found.tsx +21 -0
  117. package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play-upload.tsx +479 -0
  118. package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play.tsx +880 -0
  119. package/Influence-MW-AdServer-12-02-2026/client/src/pages/reports.tsx +235 -0
  120. package/Influence-MW-AdServer-12-02-2026/client/src/pages/settings.tsx +652 -0
  121. package/Influence-MW-AdServer-12-02-2026/client/src/pages/signal-form.tsx +1117 -0
  122. package/Influence-MW-AdServer-12-02-2026/client/src/pages/signals.tsx +366 -0
  123. package/Influence-MW-AdServer-12-02-2026/client/src/pages/tags.tsx +332 -0
  124. package/Influence-MW-AdServer-12-02-2026/client/src/pages/venues.tsx +381 -0
  125. package/Influence-MW-AdServer-12-02-2026/client/src/types/mapbox-gl-draw.d.ts +37 -0
  126. package/Influence-MW-AdServer-12-02-2026/client/src/types/react-simple-maps.d.ts +57 -0
  127. package/Influence-MW-AdServer-12-02-2026/components.json +20 -0
  128. package/Influence-MW-AdServer-12-02-2026/docs/PRD.md +3373 -0
  129. package/Influence-MW-AdServer-12-02-2026/docs/influence-feature-mapping.csv +498 -0
  130. package/Influence-MW-AdServer-12-02-2026/drizzle.config.ts +14 -0
  131. package/Influence-MW-AdServer-12-02-2026/package-lock.json +9672 -0
  132. package/Influence-MW-AdServer-12-02-2026/package.json +118 -0
  133. package/Influence-MW-AdServer-12-02-2026/postcss.config.js +6 -0
  134. package/Influence-MW-AdServer-12-02-2026/replit.md +91 -0
  135. package/Influence-MW-AdServer-12-02-2026/script/build.ts +67 -0
  136. package/Influence-MW-AdServer-12-02-2026/scripts/create-miro-diagrams.cjs +318 -0
  137. package/Influence-MW-AdServer-12-02-2026/scripts/create-remaining-diagrams.cjs +270 -0
  138. package/Influence-MW-AdServer-12-02-2026/server/index.ts +103 -0
  139. package/Influence-MW-AdServer-12-02-2026/server/recommendation-service.ts +319 -0
  140. package/Influence-MW-AdServer-12-02-2026/server/routes.ts +1890 -0
  141. package/Influence-MW-AdServer-12-02-2026/server/static.ts +19 -0
  142. package/Influence-MW-AdServer-12-02-2026/server/storage.ts +2058 -0
  143. package/Influence-MW-AdServer-12-02-2026/server/vite.ts +58 -0
  144. package/Influence-MW-AdServer-12-02-2026/shared/schema.ts +1595 -0
  145. package/Influence-MW-AdServer-12-02-2026/tailwind.config.ts +107 -0
  146. package/Influence-MW-AdServer-12-02-2026/tsconfig.json +23 -0
  147. package/Influence-MW-AdServer-12-02-2026/vite.config.ts +40 -0
  148. package/LINE_ITEM_BUDGET_FIELD_MAPPING.md +178 -0
  149. package/PCM/.env.example +92 -0
  150. package/PCM/README.md +558 -0
  151. package/PCM/docs/TEST_CASES.md +422 -0
  152. package/PCM/index.js +106 -0
  153. package/PCM/package-lock.json +3282 -0
  154. package/PCM/package.json +32 -0
  155. package/PCM/replit.md +64 -0
  156. package/PCM/schema.sql +495 -0
  157. package/PCM/scripts/export-schema.js +183 -0
  158. package/PCM/scripts/seed-comprehensive.js +631 -0
  159. package/PCM/scripts/seed-production.js +477 -0
  160. package/PCM/src/config/db.js +56 -0
  161. package/PCM/src/config/swagger.js +5975 -0
  162. package/PCM/src/dto/EmailRequestDTO.js +166 -0
  163. package/PCM/src/middleware/errorHandler.js +52 -0
  164. package/PCM/src/middleware/logger.js +26 -0
  165. package/PCM/src/migrations/001_add_campaign_mode_fields.sql +36 -0
  166. package/PCM/src/migrations/002_create_deal_id_counters.sql +22 -0
  167. package/PCM/src/migrations/003_update_publishers_column.sql +15 -0
  168. package/PCM/src/migrations/004_add_direct_dealtype_and_advertiser.sql +5 -0
  169. package/PCM/src/migrations/005_add_programmatic_fields_and_update_enums.sql +31 -0
  170. package/PCM/src/migrations/006_add_line_item_programmatic_fields.sql +12 -0
  171. package/PCM/src/migrations/007_add_line_item_direct_fields.sql +15 -0
  172. package/PCM/src/migrations/008_add_inventory_fields.sql +45 -0
  173. package/PCM/src/migrations/009_move_inventory_fields_to_metadata.sql +32 -0
  174. package/PCM/src/migrations/010_add_draft_status_and_line_items_count.sql +23 -0
  175. package/PCM/src/migrations/011_add_planning_field.sql +21 -0
  176. package/PCM/src/migrations/012_fix_inventory_composite_pk.sql +17 -0
  177. package/PCM/src/migrations/013_make_external_id_optional.sql +3 -0
  178. package/PCM/src/migrations/014_create_change_history.sql +38 -0
  179. package/PCM/src/migrations/016_create_publisher_insertion_orders.sql +33 -0
  180. package/PCM/src/migrations/017_fix_line_item_id_fk_reference.sql +86 -0
  181. package/PCM/src/migrations/018_create_approval_tables.sql +44 -0
  182. package/PCM/src/migrations/019_add_encrypted_token_column.sql +2 -0
  183. package/PCM/src/migrations/020_add_rejection_reason_to_deals.sql +10 -0
  184. package/PCM/src/migrations/021_add_publisher_external_id_to_inventories.sql +12 -0
  185. package/PCM/src/migrations/022_add_line_item_extended_fields.sql +24 -0
  186. package/PCM/src/migrations/023_add_base_price_fields.sql +8 -0
  187. package/PCM/src/migrations/run-migrations.js +46 -0
  188. package/PCM/src/models/ApprovalOTP.js +51 -0
  189. package/PCM/src/models/ApprovalToken.js +79 -0
  190. package/PCM/src/models/ChangeHistory.js +107 -0
  191. package/PCM/src/models/Deal.js +186 -0
  192. package/PCM/src/models/DealIdCounter.js +28 -0
  193. package/PCM/src/models/LineItem.js +227 -0
  194. package/PCM/src/models/LineItemCreative.js +89 -0
  195. package/PCM/src/models/LineItemInventory.js +115 -0
  196. package/PCM/src/models/PublisherInsertionOrder.js +93 -0
  197. package/PCM/src/models/TransactionHistory.js +34 -0
  198. package/PCM/src/models/associations.js +81 -0
  199. package/PCM/src/routes/approval.js +321 -0
  200. package/PCM/src/routes/creatives.js +437 -0
  201. package/PCM/src/routes/deals.js +1638 -0
  202. package/PCM/src/routes/digitalSignage.js +242 -0
  203. package/PCM/src/routes/insertionOrders.js +380 -0
  204. package/PCM/src/routes/lineItems.js +926 -0
  205. package/PCM/src/routes/system.js +384 -0
  206. package/PCM/src/services/ApprovalService.js +885 -0
  207. package/PCM/src/services/CampaignImportConverter.js +631 -0
  208. package/PCM/src/services/CampaignModeService.js +273 -0
  209. package/PCM/src/services/CampaignStatusService.js +395 -0
  210. package/PCM/src/services/ChangeHistoryService.js +316 -0
  211. package/PCM/src/services/DealIdService.js +94 -0
  212. package/PCM/src/services/DealResponseFormatter.js +90 -0
  213. package/PCM/src/services/EmailNotificationService.js +315 -0
  214. package/PCM/src/services/LineItemResponseFormatter.js +122 -0
  215. package/PCM/src/services/LineItemStatusService.js +380 -0
  216. package/PCM/src/tests/comprehensiveTestRunner.js +360 -0
  217. package/PCM/src/tests/comprehensiveTests.js +1277 -0
  218. package/PCM/src/tests/dealTypeUnitTests.js +1058 -0
  219. package/PCM/src/tests/testRunner.js +248 -0
  220. package/PCM/src/utils/caseConverter.js +92 -0
  221. package/PCM/src/utils/dealCalculations.js +206 -0
  222. package/PCM/src/utils/lineItemPayloadNormalizer.js +41 -0
  223. package/PCM/src/utils/payloadNormalizer.js +34 -0
  224. package/PCM/src/utils/sourceNormalizer.js +56 -0
  225. package/PCM/src/validators/creativeValidator.js +27 -0
  226. package/PCM/src/validators/dealValidator.js +203 -0
  227. package/PCM/src/validators/lineItemValidator.js +489 -0
  228. package/PCM/tests/approval-flows.test.js +238 -0
  229. package/PCM/tests/approval-workflow.test.js +291 -0
  230. package/PCM/tests/campaign-import-converter.test.js +543 -0
  231. package/PCM/tests/campaign-import-e2e.test.js +520 -0
  232. package/PCM/tests/campaign-status.test.js +539 -0
  233. package/PCM/tests/direct-publisher-split-reimport.test.js +460 -0
  234. package/PCM/tests/e2e/digital-signage.test.js +145 -0
  235. package/PCM/tests/e2e/search-filter-pagination.test.js +399 -0
  236. package/PCM/tests/e2e-comprehensive.test.js +3446 -0
  237. package/PCM/tests/edge-cases.test.js +340 -0
  238. package/PCM/tests/line-item-status.test.js +340 -0
  239. package/PCM/tests/seller-account-external-ids.test.js +877 -0
  240. package/PCM/tests/source-validation.test.js +324 -0
  241. package/PRD.md +3373 -0
  242. package/README.md +186 -0
  243. package/client/index.html +35 -0
  244. package/client/public/DEMO_STATUS.md +579 -0
  245. package/client/public/img/MW-logo-trans_1754045676555.png +0 -0
  246. package/client/public/locales/ar/approval.json +144 -0
  247. package/client/public/locales/ar/buyer.json +61 -0
  248. package/client/public/locales/ar/campaigns.json +1 -0
  249. package/client/public/locales/ar/common.json +218 -0
  250. package/client/public/locales/ar/contentHub.json +266 -0
  251. package/client/public/locales/ar/creatives.json +79 -0
  252. package/client/public/locales/ar/dashboard.json +57 -0
  253. package/client/public/locales/ar/deals.json +886 -0
  254. package/client/public/locales/ar/dsp.json +131 -0
  255. package/client/public/locales/ar/inventory.json +201 -0
  256. package/client/public/locales/ar/lineItems.json +553 -0
  257. package/client/public/locales/ar/navigation.json +48 -0
  258. package/client/public/locales/ar/wizard.json +1 -0
  259. package/client/public/locales/en/approval.json +144 -0
  260. package/client/public/locales/en/buyer.json +65 -0
  261. package/client/public/locales/en/campaigns.json +1 -0
  262. package/client/public/locales/en/common.json +218 -0
  263. package/client/public/locales/en/contentHub.json +266 -0
  264. package/client/public/locales/en/creatives.json +79 -0
  265. package/client/public/locales/en/dashboard.json +57 -0
  266. package/client/public/locales/en/deals.json +886 -0
  267. package/client/public/locales/en/dsp.json +131 -0
  268. package/client/public/locales/en/inventory.json +201 -0
  269. package/client/public/locales/en/lineItems.json +659 -0
  270. package/client/public/locales/en/navigation.json +48 -0
  271. package/client/public/locales/en/wizard.json +1 -0
  272. package/client/public/locales/ja/approval.json +144 -0
  273. package/client/public/locales/ja/buyer.json +61 -0
  274. package/client/public/locales/ja/campaigns.json +1 -0
  275. package/client/public/locales/ja/common.json +218 -0
  276. package/client/public/locales/ja/contentHub.json +266 -0
  277. package/client/public/locales/ja/creatives.json +79 -0
  278. package/client/public/locales/ja/dashboard.json +57 -0
  279. package/client/public/locales/ja/deals.json +886 -0
  280. package/client/public/locales/ja/dsp.json +131 -0
  281. package/client/public/locales/ja/inventory.json +201 -0
  282. package/client/public/locales/ja/lineItems.json +553 -0
  283. package/client/public/locales/ja/navigation.json +48 -0
  284. package/client/public/locales/ja/wizard.json +1 -0
  285. package/client/public/locales/zh/approval.json +144 -0
  286. package/client/public/locales/zh/buyer.json +61 -0
  287. package/client/public/locales/zh/campaigns.json +1 -0
  288. package/client/public/locales/zh/common.json +218 -0
  289. package/client/public/locales/zh/contentHub.json +266 -0
  290. package/client/public/locales/zh/creatives.json +79 -0
  291. package/client/public/locales/zh/dashboard.json +57 -0
  292. package/client/public/locales/zh/deals.json +886 -0
  293. package/client/public/locales/zh/dsp.json +131 -0
  294. package/client/public/locales/zh/inventory.json +201 -0
  295. package/client/public/locales/zh/lineItems.json +553 -0
  296. package/client/public/locales/zh/navigation.json +48 -0
  297. package/client/public/locales/zh/wizard.json +1 -0
  298. package/client/public/manifest.json +36 -0
  299. package/client/src/App.tsx +464 -0
  300. package/client/src/components/app-sidebar.tsx +312 -0
  301. package/client/src/components/approval/approval-decision-form.test.tsx +294 -0
  302. package/client/src/components/approval/approval-decision-form.tsx +326 -0
  303. package/client/src/components/approval/approval-sheet.tsx +631 -0
  304. package/client/src/components/approval/line-item-details-sheet.tsx +371 -0
  305. package/client/src/components/approval/otp-verification.test.tsx +337 -0
  306. package/client/src/components/approval/otp-verification.tsx +180 -0
  307. package/client/src/components/content-hub/bulk-transcode-dialog.tsx +379 -0
  308. package/client/src/components/content-hub/content-hub-manager-v2.tsx +574 -0
  309. package/client/src/components/content-hub/content-hub-manager.tsx +330 -0
  310. package/client/src/components/content-hub/creative-card.tsx +456 -0
  311. package/client/src/components/content-hub/creative-detail-sheet.tsx +685 -0
  312. package/client/src/components/content-hub/creative-filters.tsx +457 -0
  313. package/client/src/components/content-hub/creative-grid.tsx +329 -0
  314. package/client/src/components/content-hub/creative-selector.tsx +415 -0
  315. package/client/src/components/content-hub/creative-upload.tsx +547 -0
  316. package/client/src/components/content-hub/folder-dialogs.tsx +445 -0
  317. package/client/src/components/content-hub/folder-list.tsx +280 -0
  318. package/client/src/components/content-hub/review-dialogs.tsx +268 -0
  319. package/client/src/components/content-hub/transcode-dialog.tsx +226 -0
  320. package/client/src/components/creative-library/creative-details-view.tsx +446 -0
  321. package/client/src/components/creative-library/creative-filters-panel.tsx +203 -0
  322. package/client/src/components/creative-library/creative-list.tsx +360 -0
  323. package/client/src/components/creative-library/creative-status-badge.tsx +71 -0
  324. package/client/src/components/creative-library/folder-card.tsx +78 -0
  325. package/client/src/components/creative-library/index.ts +27 -0
  326. package/client/src/components/creative-library/new-creative-card.tsx +211 -0
  327. package/client/src/components/creative-library/upload-creative-dialog.tsx +261 -0
  328. package/client/src/components/dashboard-overview.tsx +109 -0
  329. package/client/src/components/deals/approval-history-panel.test.tsx +240 -0
  330. package/client/src/components/deals/approval-history-panel.tsx +156 -0
  331. package/client/src/components/deals/deal-status-badge.tsx +92 -0
  332. package/client/src/components/deals/import-from-planner-dialog.tsx +399 -0
  333. package/client/src/components/deals/market-insights-panel.tsx +237 -0
  334. package/client/src/components/deals/reopen-deal-sheet.tsx +191 -0
  335. package/client/src/components/deals/request-approval-sheet.test.tsx +323 -0
  336. package/client/src/components/deals/request-approval-sheet.tsx +136 -0
  337. package/client/src/components/deals/resend-approval-sheet.tsx +201 -0
  338. package/client/src/components/direct-campaigns/campaign-card.tsx +283 -0
  339. package/client/src/components/direct-campaigns/deal-filter-panel.tsx +325 -0
  340. package/client/src/components/inventory/advanced-filters-panel.tsx +273 -0
  341. package/client/src/components/inventory/csv-upload-modal.tsx +639 -0
  342. package/client/src/components/inventory/inventory-availability-view.tsx +486 -0
  343. package/client/src/components/inventory/inventory-details-sheet.tsx +376 -0
  344. package/client/src/components/inventory/inventory-map-view.tsx +596 -0
  345. package/client/src/components/inventory/inventory-settings-menu.tsx +52 -0
  346. package/client/src/components/language-switcher.tsx +53 -0
  347. package/client/src/components/line-items/campaign-forecast-panel.tsx +138 -0
  348. package/client/src/components/line-items/form-insights.tsx +89 -0
  349. package/client/src/components/line-items/geofencing/LocationCsvUploadDrawer.tsx +100 -0
  350. package/client/src/components/line-items/geofencing/POIDropdown.tsx +379 -0
  351. package/client/src/components/line-items/geofencing/SelectedLocationsSidebar.tsx +436 -0
  352. package/client/src/components/line-items/geofencing/ViewFileLocationDrawer.tsx +199 -0
  353. package/client/src/components/line-items/geofencing/components/ExistingFilesTab.tsx +268 -0
  354. package/client/src/components/line-items/geofencing/components/TemplateDownloadSection.tsx +59 -0
  355. package/client/src/components/line-items/geofencing/components/UploadTab.tsx +215 -0
  356. package/client/src/components/line-items/geofencing-map.tsx +1270 -0
  357. package/client/src/components/line-items/inventory-availability-section.tsx +178 -0
  358. package/client/src/components/line-items/line-item-schedule-manager.tsx +313 -0
  359. package/client/src/components/line-items/manual-inventory-drawer.tsx +346 -0
  360. package/client/src/components/line-items/planner-inventory-card.tsx +495 -0
  361. package/client/src/components/line-items/planner-schedule-grid.tsx +495 -0
  362. package/client/src/components/line-items/schedule-rule-editor.tsx +649 -0
  363. package/client/src/components/line-items/schedule-rule-types.ts +122 -0
  364. package/client/src/components/line-items/steps/creatives-step.tsx +681 -0
  365. package/client/src/components/line-items/steps/inventory-schedule-step.tsx +1596 -0
  366. package/client/src/components/line-items/steps/inventory-step.tsx +1533 -0
  367. package/client/src/components/line-items/steps/line-item-details-step.tsx +916 -0
  368. package/client/src/components/line-items/steps/schedule-step.tsx +273 -0
  369. package/client/src/components/line-items/steps/summary-step.tsx +680 -0
  370. package/client/src/components/line-items/steps/targeting-step.tsx +1708 -0
  371. package/client/src/components/product-switcher.tsx +105 -0
  372. package/client/src/components/protected-route.tsx +49 -0
  373. package/client/src/components/skip-link.tsx +22 -0
  374. package/client/src/components/stat-card.tsx +53 -0
  375. package/client/src/components/status-badge.tsx +96 -0
  376. package/client/src/components/ui/hierarchical-venue-selector.tsx +389 -0
  377. package/client/src/components/ui/toaster.tsx +111 -0
  378. package/client/src/contexts/auth-context.tsx +181 -0
  379. package/client/src/contexts/sidebar-state.tsx +50 -0
  380. package/client/src/contexts/theme-context.tsx +66 -0
  381. package/client/src/data/campaign-data.json +107 -0
  382. package/client/src/data/countries.json +22 -0
  383. package/client/src/hooks/use-approval.ts +366 -0
  384. package/client/src/hooks/use-keyboard-shortcuts.ts +74 -0
  385. package/client/src/hooks/use-media-query.ts +46 -0
  386. package/client/src/hooks/use-mobile.tsx +19 -0
  387. package/client/src/hooks/use-page-title.ts +21 -0
  388. package/client/src/hooks/use-toast.ts +195 -0
  389. package/client/src/index.css +694 -0
  390. package/client/src/lib/__tests__/accessibility.test.ts +104 -0
  391. package/client/src/lib/__tests__/date-utils.test.ts +199 -0
  392. package/client/src/lib/__tests__/dsp-buyer-api.test.ts +127 -0
  393. package/client/src/lib/__tests__/dsp-buyer-integration.test.ts +247 -0
  394. package/client/src/lib/__tests__/storage-utils.test.ts +167 -0
  395. package/client/src/lib/__tests__/utils.test.ts +57 -0
  396. package/client/src/lib/accessibility.ts +141 -0
  397. package/client/src/lib/api-config.ts +9 -0
  398. package/client/src/lib/auth-service.ts +209 -0
  399. package/client/src/lib/campaign-creative-api.ts +82 -0
  400. package/client/src/lib/company-api.ts +61 -0
  401. package/client/src/lib/content-hub-api.ts +407 -0
  402. package/client/src/lib/creative-mapper.ts +61 -0
  403. package/client/src/lib/date-utils.ts +119 -0
  404. package/client/src/lib/deal-helpers.ts +220 -0
  405. package/client/src/lib/dsp-buyer-api.ts +196 -0
  406. package/client/src/lib/geo-import-api.ts +151 -0
  407. package/client/src/lib/google-poi-api.ts +305 -0
  408. package/client/src/lib/i18n/__tests__/formatting.test.ts +202 -0
  409. package/client/src/lib/i18n/formatting.ts +130 -0
  410. package/client/src/lib/i18n/index.ts +8 -0
  411. package/client/src/lib/i18n-compat.ts +76 -0
  412. package/client/src/lib/influence-deals-api.ts +896 -0
  413. package/client/src/lib/inventory-api.ts +399 -0
  414. package/client/src/lib/oauth-service.ts +678 -0
  415. package/client/src/lib/poi-types.ts +75 -0
  416. package/client/src/lib/queryClient.ts +144 -0
  417. package/client/src/lib/recommendation-api.ts +380 -0
  418. package/client/src/lib/storage-utils.ts +104 -0
  419. package/client/src/lib/tolgee.ts +85 -0
  420. package/client/src/lib/utils.ts +0 -0
  421. package/client/src/main.tsx +67 -0
  422. package/client/src/mapbox-draw-modes.d.ts +32 -0
  423. package/client/src/pages/all-folders.tsx +203 -0
  424. package/client/src/pages/auth-callback.tsx +115 -0
  425. package/client/src/pages/buyer-form.tsx +339 -0
  426. package/client/src/pages/buyer-list.tsx +622 -0
  427. package/client/src/pages/content-hub.tsx +1358 -0
  428. package/client/src/pages/create-deal.tsx +2093 -0
  429. package/client/src/pages/creative-assignment-page.tsx +548 -0
  430. package/client/src/pages/creatives.tsx +5 -0
  431. package/client/src/pages/custom-pois.tsx +425 -0
  432. package/client/src/pages/dashboard.tsx +615 -0
  433. package/client/src/pages/deal-history.tsx +434 -0
  434. package/client/src/pages/deal-line-items.tsx +1703 -0
  435. package/client/src/pages/demo-status.tsx +113 -0
  436. package/client/src/pages/direct-campaign-details.tsx +361 -0
  437. package/client/src/pages/direct-campaigns-new.tsx +824 -0
  438. package/client/src/pages/dsp-form.tsx +803 -0
  439. package/client/src/pages/dsp-list.tsx +239 -0
  440. package/client/src/pages/folder-content.tsx +336 -0
  441. package/client/src/pages/integrations.tsx +429 -0
  442. package/client/src/pages/line-item-creatives.tsx +789 -0
  443. package/client/src/pages/line-item-detail-page.tsx +684 -0
  444. package/client/src/pages/line-item-form-page.tsx +3261 -0
  445. package/client/src/pages/line-item-wizard.tsx +1207 -0
  446. package/client/src/pages/login.tsx +154 -0
  447. package/client/src/pages/not-found.tsx +23 -0
  448. package/client/src/pages/proof-of-play.tsx +397 -0
  449. package/client/src/pages/public-approval.tsx +551 -0
  450. package/client/src/pages/reports.tsx +231 -0
  451. package/client/src/pages/settings.tsx +760 -0
  452. package/client/src/pages/signals.tsx +389 -0
  453. package/client/src/pages/tags.tsx +318 -0
  454. package/client/src/pages/test-results.tsx +328 -0
  455. package/client/src/store/hooks.ts +5 -0
  456. package/client/src/store/index.ts +15 -0
  457. package/client/src/store/mapMarkerLocationsSlice.ts +241 -0
  458. package/client/src/styles/design-tokens.css +324 -0
  459. package/client/src/test/setup.ts +261 -0
  460. package/client/src/test/test-utils.tsx +40 -0
  461. package/client/src/types/approval.ts +221 -0
  462. package/client/src/types/content-hub.ts +209 -0
  463. package/client/src/types/geofencing.ts +67 -0
  464. package/client/src/types/transcoding.ts +140 -0
  465. package/client/src/vite-env.d.ts +18 -0
  466. package/components.json +20 -0
  467. package/creative-api.json +1 -0
  468. package/docs/AI_REFERENCE.md +459 -0
  469. package/docs/MWDesign-Prompt.md +132 -0
  470. package/docs/MWDesign-System.md +344 -0
  471. package/docs/test-plan.md +277 -0
  472. package/e2e/AUTONOMOUS-TESTING.md +406 -0
  473. package/e2e/README.md +219 -0
  474. package/e2e/autonomous-flow.spec.ts +308 -0
  475. package/e2e/debug-sso.spec.ts +163 -0
  476. package/e2e/direct-campaigns.spec.ts +219 -0
  477. package/e2e/explore-sso.spec.ts +149 -0
  478. package/e2e/fixtures/auth.ts +26 -0
  479. package/e2e/fixtures/enhanced-test.ts +331 -0
  480. package/e2e/pagination.spec.ts +280 -0
  481. package/e2e/view-toggle.spec.ts +312 -0
  482. package/generated-icon.png +0 -0
  483. package/i18next-scanner.config.cjs +46 -0
  484. package/package.json +141 -0
  485. package/playwright.config.ts +93 -0
  486. package/postcss.config.js +6 -0
  487. package/replit.md +196 -0
  488. package/screenshot-after-login.png +0 -0
  489. package/screenshot-contenthub-grid.png +0 -0
  490. package/screenshot-contenthub-list-fixed.png +0 -0
  491. package/screenshot-contenthub-list.png +0 -0
  492. package/screenshot-create-deal.png +0 -0
  493. package/screenshot-dashboard.png +0 -0
  494. package/screenshot-deals.png +0 -0
  495. package/screenshot-login-filled.png +0 -0
  496. package/screenshot-login.png +0 -0
  497. package/screenshot.mjs +24 -0
  498. package/scripts/deploy-stg.sh +185 -0
  499. package/shared/direct-io-schema.ts +383 -0
  500. package/shared/schema.ts +439 -0
  501. package/shared/screen-types.ts +149 -0
  502. package/springdocDefault.json +1 -0
  503. package/swagger-ui-bundle.js +2 -0
  504. package/swagger-ui-init.js +10316 -0
  505. package/tailwind.config.ts +282 -0
  506. package/terraform/README.md +306 -0
  507. package/terraform/cloudfront.tf +289 -0
  508. package/terraform/ecs.tf +727 -0
  509. package/terraform/environments/dev.tfvars +59 -0
  510. package/terraform/environments/production.tfvars +60 -0
  511. package/terraform/main.tf +47 -0
  512. package/terraform/outputs.tf +145 -0
  513. package/terraform/s3.tf +192 -0
  514. package/terraform/variables.tf +226 -0
  515. package/terraform/waf.tf +165 -0
  516. package/terraform-frontend/.terraform.lock.hcl +25 -0
  517. package/terraform-frontend/README.md +85 -0
  518. package/terraform-frontend/cloudfront.tf +125 -0
  519. package/terraform-frontend/main.tf +31 -0
  520. package/terraform-frontend/outputs.tf +24 -0
  521. package/terraform-frontend/terraform.tfvars +12 -0
  522. package/terraform-frontend/variables.tf +53 -0
  523. package/tsconfig.json +23 -0
  524. package/vite.config.ts +226 -0
  525. package/vitest.config.ts +56 -0
@@ -0,0 +1,460 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ const BASE_URL = 'http://localhost:5000/api/v1/rest';
5
+
6
+ async function request(method, path, body = null) {
7
+ const options = {
8
+ method,
9
+ headers: { 'Content-Type': 'application/json' }
10
+ };
11
+ if (body) {
12
+ options.body = JSON.stringify(body);
13
+ }
14
+ const response = await fetch(`${BASE_URL}${path}`, options);
15
+ const data = await response.json();
16
+ return { status: response.status, data };
17
+ }
18
+
19
+ function createImportPayload(campaignId, inventories, overrides = {}) {
20
+ return {
21
+ payloadType: 'DIRECT_PUBLISHER_SPLIT',
22
+ externalPayload: {
23
+ campaign: {
24
+ externalId: campaignId,
25
+ name: overrides.name || `Reimport Test Campaign ${campaignId}`,
26
+ brand: 'Test Brand',
27
+ clientType: 'AGENCY',
28
+ approvalEmails: ['test@example.com'],
29
+ marketSelection: { country: 'MY', currency: 'MYR' },
30
+ budgetSetup: { currency: 'MYR', budgetAmount: 100000 },
31
+ campaignGoal: { type: 'IMPRESSIONS', targetValue: 1000000 },
32
+ startDate: '2026-01-01T00:00:00Z',
33
+ endDate: '2026-12-31T23:59:59Z',
34
+ ...overrides.campaign
35
+ },
36
+ inventories
37
+ },
38
+ options: { source: overrides.source || 'external-test' }
39
+ };
40
+ }
41
+
42
+ function createInternalDealPayload(externalId, name = 'Internal Test Deal') {
43
+ return {
44
+ source: 'influence',
45
+ externalId,
46
+ name,
47
+ mode: 'DIRECT',
48
+ dealType: 'GUARANTEED',
49
+ status: 'DRAFT',
50
+ brand: 'Test Brand',
51
+ clientType: 'AGENCY',
52
+ currency: 'MYR',
53
+ marketSelection: { country: 'MY', currency: 'MYR' },
54
+ budgetSetup: { currency: 'MYR', budgetAmount: 10000 },
55
+ campaignGoal: { type: 'IMPRESSIONS', targetValue: 100000 }
56
+ };
57
+ }
58
+
59
+ async function reopenDeal(dealId) {
60
+ await request('PUT', `/deals/${dealId}`, { status: 'APPROVED' });
61
+ const { status, data } = await request('POST', `/deals/${dealId}/reopen`, { reason: 'For testing' });
62
+ return { status, data };
63
+ }
64
+
65
+ const createdDeals = [];
66
+
67
+ describe('DIRECT_PUBLISHER_SPLIT Comprehensive Test Suite', () => {
68
+
69
+ after(async () => {
70
+ for (const dealId of createdDeals) {
71
+ try {
72
+ await request('DELETE', `/deals/${dealId}`);
73
+ } catch (e) {}
74
+ }
75
+ });
76
+
77
+ describe('1. Fresh Import (INSERT) Tests', () => {
78
+ it('should create new deal with external source', async () => {
79
+ const campaignId = `fresh-ext-${Date.now()}`;
80
+ const inventories = [
81
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
82
+ ];
83
+
84
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
85
+ assert.strictEqual(status, 201);
86
+ createdDeals.push(data.result.deal.id);
87
+
88
+ assert.strictEqual(data.result.summary.isUpdate, false);
89
+ assert.strictEqual(data.result.deal.source, 'external-test');
90
+ assert.strictEqual(data.result.deal.mode, 'DIRECT');
91
+ });
92
+
93
+ it('should create multiple line items for multiple publishers', async () => {
94
+ const campaignId = `multi-pub-${Date.now()}`;
95
+ const inventories = [
96
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
97
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two' } },
98
+ { id: 'inv-3', name: 'Screen 3', resolution: '3840x2160', publisher: { id: 'pub-001', name: 'Publisher One' } }
99
+ ];
100
+
101
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
102
+ assert.strictEqual(status, 201);
103
+ createdDeals.push(data.result.deal.id);
104
+
105
+ assert.strictEqual(data.result.summary.lineItemsProcessed, 2, 'Should create 2 line items (grouped by publisher+duration)');
106
+ assert.strictEqual(data.result.summary.insertionOrdersCreated, 2, 'Should create 2 insertion orders');
107
+ });
108
+
109
+ it('should populate publishers array from inventories', async () => {
110
+ const campaignId = `publishers-${Date.now()}`;
111
+ const inventories = [
112
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One', externalId: 'EXT-001' } },
113
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two', externalId: 'EXT-002' } }
114
+ ];
115
+
116
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
117
+ assert.strictEqual(status, 201);
118
+ createdDeals.push(data.result.deal.id);
119
+
120
+ assert.strictEqual(data.result.deal.publishers.length, 2);
121
+ const pub1 = data.result.deal.publishers.find(p => p.id === 'pub-001');
122
+ assert.strictEqual(pub1.externalId, 'EXT-001');
123
+ });
124
+
125
+ it('should create insertion orders for each publisher', async () => {
126
+ const campaignId = `ios-${Date.now()}`;
127
+ const inventories = [
128
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
129
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two' } }
130
+ ];
131
+
132
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
133
+ assert.strictEqual(status, 201);
134
+ createdDeals.push(data.result.deal.id);
135
+ const dealId = data.result.deal.dealId;
136
+
137
+ const { data: dealData } = await request('GET', `/deals/${dealId}`);
138
+ assert.strictEqual(dealData.result.insertionOrders.length, 2);
139
+ });
140
+ });
141
+
142
+ describe('2. Upsert Status Restriction Tests', () => {
143
+ it('should reject upsert when deal is in DRAFT status', async () => {
144
+ const campaignId = `draft-reject-${Date.now()}`;
145
+ const inventories = [
146
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
147
+ ];
148
+
149
+ const { status: status1, data: data1 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
150
+ assert.strictEqual(status1, 201);
151
+ createdDeals.push(data1.result.deal.id);
152
+
153
+ const { status: status2, data: data2 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
154
+
155
+ assert.strictEqual(status2, 409);
156
+ assert.strictEqual(data2.code, 'UPSERT_NOT_ALLOWED');
157
+ assert.strictEqual(data2.currentStatus, 'DRAFT');
158
+ assert.strictEqual(data2.requiredStatus, 'REOPENED');
159
+ });
160
+
161
+ it('should reject upsert when deal is in APPROVED status', async () => {
162
+ const campaignId = `approved-reject-${Date.now()}`;
163
+ const inventories = [
164
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
165
+ ];
166
+
167
+ const { status: status1, data: data1 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
168
+ assert.strictEqual(status1, 201);
169
+ createdDeals.push(data1.result.deal.id);
170
+ const dealId = data1.result.deal.dealId;
171
+
172
+ await request('PUT', `/deals/${dealId}`, { status: 'APPROVED' });
173
+
174
+ const { status: status2, data: data2 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
175
+
176
+ assert.strictEqual(status2, 409);
177
+ assert.strictEqual(data2.currentStatus, 'APPROVED');
178
+ });
179
+
180
+ it('should allow upsert when deal is in REOPENED status', async () => {
181
+ const campaignId = `reopened-allow-${Date.now()}`;
182
+ const inventories = [
183
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
184
+ ];
185
+
186
+ const { status: status1, data: data1 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
187
+ assert.strictEqual(status1, 201);
188
+ createdDeals.push(data1.result.deal.id);
189
+ const dealId = data1.result.deal.dealId;
190
+
191
+ const { status: reopenStatus } = await reopenDeal(dealId);
192
+ assert.strictEqual(reopenStatus, 200, 'Reopen should succeed');
193
+
194
+ const newInventories = [
195
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
196
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two' } }
197
+ ];
198
+
199
+ const { status: status2, data: data2 } = await request('POST', '/deals/import', createImportPayload(campaignId, newInventories));
200
+
201
+ assert.strictEqual(status2, 201, `Upsert should succeed: ${JSON.stringify(data2)}`);
202
+ assert.strictEqual(data2.result.summary.isUpdate, true);
203
+ });
204
+ });
205
+
206
+ describe('3. External Source Re-Import Tests', () => {
207
+ it('should allow external source to reopen and re-import', async () => {
208
+ const campaignId = `ext-reimport-${Date.now()}`;
209
+ const inventories = [
210
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
211
+ ];
212
+
213
+ const { status: status1, data: data1 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories, { source: 'broadsign' }));
214
+ assert.strictEqual(status1, 201);
215
+ createdDeals.push(data1.result.deal.id);
216
+ const dealId = data1.result.deal.dealId;
217
+
218
+ const { status: reopenStatus, data: reopenData } = await reopenDeal(dealId);
219
+ assert.strictEqual(reopenStatus, 200, `Reopen failed: ${JSON.stringify(reopenData)}`);
220
+
221
+ const newInventories = [
222
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
223
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two' } }
224
+ ];
225
+
226
+ const { status: status2, data: data2 } = await request('POST', '/deals/import', createImportPayload(campaignId, newInventories, { source: 'broadsign' }));
227
+
228
+ assert.strictEqual(status2, 201, `Re-import failed: ${JSON.stringify(data2)}`);
229
+ assert.strictEqual(data2.result.summary.isUpdate, true);
230
+ assert.strictEqual(data2.result.summary.insertionOrdersCreated, 1);
231
+ });
232
+ });
233
+
234
+ describe('4. Archiving Logic Tests', () => {
235
+ it('should archive line items when publisher is removed', async () => {
236
+ const campaignId = `archive-li-${Date.now()}`;
237
+ const inventories = [
238
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
239
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two' } }
240
+ ];
241
+
242
+ const { status: status1, data: data1 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
243
+ assert.strictEqual(status1, 201);
244
+ createdDeals.push(data1.result.deal.id);
245
+ const dealId = data1.result.deal.dealId;
246
+
247
+ await reopenDeal(dealId);
248
+
249
+ const reducedInventories = [
250
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
251
+ ];
252
+
253
+ const { status: status2, data: data2 } = await request('POST', '/deals/import', createImportPayload(campaignId, reducedInventories));
254
+ assert.strictEqual(status2, 201);
255
+ assert.strictEqual(data2.result.summary.lineItemsArchived, 1);
256
+
257
+ const { data: dealData } = await request('GET', `/deals/${dealId}?embed=lineItems`);
258
+ const archivedItems = dealData.result.lineItems.filter(li => li.status === 'ARCHIVED');
259
+ assert.strictEqual(archivedItems.length, 1);
260
+ });
261
+
262
+ it('should archive insertion orders when publisher is removed', async () => {
263
+ const campaignId = `archive-io-${Date.now()}`;
264
+ const inventories = [
265
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
266
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two' } }
267
+ ];
268
+
269
+ const { status: status1, data: data1 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
270
+ assert.strictEqual(status1, 201);
271
+ createdDeals.push(data1.result.deal.id);
272
+ const dealId = data1.result.deal.dealId;
273
+
274
+ await reopenDeal(dealId);
275
+
276
+ const reducedInventories = [
277
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
278
+ ];
279
+
280
+ const { status: status2, data: data2 } = await request('POST', '/deals/import', createImportPayload(campaignId, reducedInventories));
281
+ assert.strictEqual(status2, 201);
282
+ assert.strictEqual(data2.result.summary.insertionOrdersArchived, 1);
283
+ });
284
+
285
+ it('should reactivate archived insertion order when publisher returns', async () => {
286
+ const campaignId = `reactivate-io-${Date.now()}`;
287
+ const inventories = [
288
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
289
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two' } }
290
+ ];
291
+
292
+ const { data: data1 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
293
+ createdDeals.push(data1.result.deal.id);
294
+ const dealId = data1.result.deal.dealId;
295
+
296
+ await reopenDeal(dealId);
297
+
298
+ await request('POST', '/deals/import', createImportPayload(campaignId, [inventories[0]]));
299
+
300
+ await reopenDeal(dealId);
301
+
302
+ const { status, data: data3 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
303
+ assert.strictEqual(status, 201);
304
+
305
+ const { data: dealData } = await request('GET', `/deals/${dealId}`);
306
+ const activeIOs = dealData.result.insertionOrders.filter(io => io.status !== 'ARCHIVED');
307
+ assert.strictEqual(activeIOs.length, 2, 'Both insertion orders should be active');
308
+ });
309
+ });
310
+
311
+ describe('5. Deterministic ExternalId Tests', () => {
312
+ it('should generate consistent externalId with inventory fingerprint', async () => {
313
+ const campaignId = `fingerprint-${Date.now()}`;
314
+ const inventories = [
315
+ { id: 'inv-a', name: 'Screen A', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
316
+ { id: 'inv-b', name: 'Screen B', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
317
+ ];
318
+
319
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
320
+ assert.strictEqual(status, 201);
321
+ createdDeals.push(data.result.deal.id);
322
+ const dealId = data.result.deal.dealId;
323
+
324
+ const { data: dealData } = await request('GET', `/deals/${dealId}?embed=lineItems`);
325
+ const externalId = dealData.result.lineItems[0]?.externalId;
326
+
327
+ assert.ok(externalId, 'Should have externalId');
328
+ const parts = externalId.split('-');
329
+ assert.ok(parts.length >= 5, 'ExternalId should include fingerprint');
330
+ });
331
+
332
+ it('should preserve line item UUID on re-import with same inventories', async () => {
333
+ const campaignId = `preserve-uuid-${Date.now()}`;
334
+ const inventories = [
335
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
336
+ ];
337
+
338
+ const { data: data1 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
339
+ createdDeals.push(data1.result.deal.id);
340
+ const dealId = data1.result.deal.dealId;
341
+
342
+ const { data: lineItems1 } = await request('GET', `/deals/${dealId}?embed=lineItems`);
343
+ const originalUUID = lineItems1.result.lineItems[0]?.id;
344
+
345
+ await reopenDeal(dealId);
346
+
347
+ await request('POST', '/deals/import', createImportPayload(campaignId, inventories, { name: 'Updated Name' }));
348
+
349
+ const { data: lineItems2 } = await request('GET', `/deals/${dealId}?embed=lineItems`);
350
+ const updatedUUID = lineItems2.result.lineItems[0]?.id;
351
+
352
+ assert.strictEqual(originalUUID, updatedUUID, 'Line item UUID should be preserved');
353
+ });
354
+
355
+ it('should handle inventories without IDs using fallback fingerprint', async () => {
356
+ const campaignId = `no-id-${Date.now()}`;
357
+ const inventories = [
358
+ { name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
359
+ { name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
360
+ ];
361
+
362
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
363
+ assert.strictEqual(status, 201);
364
+ createdDeals.push(data.result.deal.id);
365
+
366
+ assert.strictEqual(data.result.summary.lineItemsProcessed, 1);
367
+ });
368
+ });
369
+
370
+ describe('6. Summary Response Accuracy Tests', () => {
371
+ it('should return accurate summary for fresh import', async () => {
372
+ const campaignId = `summary-fresh-${Date.now()}`;
373
+ const inventories = [
374
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
375
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two' } }
376
+ ];
377
+
378
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
379
+ assert.strictEqual(status, 201);
380
+ createdDeals.push(data.result.deal.id);
381
+
382
+ const summary = data.result.summary;
383
+ assert.strictEqual(summary.isUpdate, false);
384
+ assert.strictEqual(summary.lineItemsProcessed, 2);
385
+ assert.strictEqual(summary.lineItemsArchived, 0);
386
+ assert.strictEqual(summary.insertionOrdersCreated, 2);
387
+ assert.strictEqual(summary.insertionOrdersArchived, 0);
388
+ });
389
+
390
+ it('should return accurate summary for update import', async () => {
391
+ const campaignId = `summary-update-${Date.now()}`;
392
+ const inventories1 = [
393
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
394
+ { id: 'inv-2', name: 'Screen 2', resolution: '1920x1080', publisher: { id: 'pub-002', name: 'Publisher Two' } }
395
+ ];
396
+
397
+ const { data: data1 } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories1));
398
+ createdDeals.push(data1.result.deal.id);
399
+ const dealId = data1.result.deal.dealId;
400
+
401
+ await reopenDeal(dealId);
402
+
403
+ const inventories2 = [
404
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } }
405
+ ];
406
+
407
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories2));
408
+ assert.strictEqual(status, 201);
409
+
410
+ const summary = data.result.summary;
411
+ assert.strictEqual(summary.isUpdate, true);
412
+ assert.strictEqual(summary.lineItemsProcessed, 1);
413
+ assert.strictEqual(summary.lineItemsArchived, 1);
414
+ assert.strictEqual(summary.insertionOrdersArchived, 1);
415
+ });
416
+ });
417
+
418
+ describe('7. Internal Source Workflow Tests', () => {
419
+ it('should allow internal source to reopen deals', async () => {
420
+ const externalId = `internal-reopen-${Date.now()}`;
421
+ const { status: createStatus, data: createData } = await request('POST', '/deals', createInternalDealPayload(externalId));
422
+ assert.strictEqual(createStatus, 201);
423
+ createdDeals.push(createData.result.id);
424
+ const dealId = createData.result.dealId;
425
+
426
+ await request('PUT', `/deals/${dealId}`, { status: 'APPROVED' });
427
+
428
+ const { status: reopenStatus, data: reopenData } = await request('POST', `/deals/${dealId}/reopen`, { reason: 'Testing' });
429
+
430
+ assert.strictEqual(reopenStatus, 200, `Reopen failed: ${JSON.stringify(reopenData)}`);
431
+ assert.strictEqual(reopenData.result.currentStatus, 'REOPENED');
432
+ });
433
+ });
434
+
435
+ describe('8. Edge Case Tests', () => {
436
+ it('should handle different resolutions for same publisher', async () => {
437
+ const campaignId = `diff-res-${Date.now()}`;
438
+ const inventories = [
439
+ { id: 'inv-1', name: 'Screen 1', resolution: '1920x1080', publisher: { id: 'pub-001', name: 'Publisher One' } },
440
+ { id: 'inv-2', name: 'Screen 2', resolution: '3840x2160', publisher: { id: 'pub-001', name: 'Publisher One' } }
441
+ ];
442
+
443
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
444
+ assert.strictEqual(status, 201);
445
+ createdDeals.push(data.result.deal.id);
446
+
447
+ assert.strictEqual(data.result.summary.lineItemsProcessed, 1, 'Should create one line item (same publisher+duration, multiple resolutions)');
448
+ assert.strictEqual(data.result.summary.insertionOrdersCreated, 1, 'Should create one insertion order for same publisher');
449
+ });
450
+
451
+ it('should reject empty inventories array', async () => {
452
+ const campaignId = `empty-inv-${Date.now()}`;
453
+ const inventories = [];
454
+
455
+ const { status, data } = await request('POST', '/deals/import', createImportPayload(campaignId, inventories));
456
+
457
+ assert.strictEqual(status, 400, 'Empty inventories should be rejected');
458
+ });
459
+ });
460
+ });
@@ -0,0 +1,145 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ const BASE_URL = 'http://localhost:5000';
5
+ const API_V2_DS = `${BASE_URL}/api/v2/rest/ds`;
6
+
7
+ const TEST_INVENTORY_ID = 'INV-INFLUENCE-000063';
8
+ const TEST_DEVICE_ID = 'DEVICE-influence-pub-stroer-63';
9
+ const TEST_REFERENCE_ID = 'REF-influence-pub-stroer-63';
10
+ const TEST_DEAL_ID = 'DIR-INFLUENCE-00005-53805';
11
+ const TEST_LINE_ITEM_ID = 'influence-li-15';
12
+
13
+ describe('Digital Signage API E2E Tests', () => {
14
+ describe('GET /deals/:identifier', () => {
15
+ it('should return 404 for non-existent identifier', async () => {
16
+ const res = await fetch(`${API_V2_DS}/deals/non-existent-device-12345`);
17
+ const data = await res.json();
18
+
19
+ assert.strictEqual(res.status, 404);
20
+ assert.strictEqual(data.status, 404);
21
+ assert.ok(data.error.includes('No inventory or device found'));
22
+ });
23
+
24
+ it('should return campaigns for valid inventory ID', async () => {
25
+ const res = await fetch(`${API_V2_DS}/deals/${TEST_INVENTORY_ID}`);
26
+ const data = await res.json();
27
+
28
+ assert.strictEqual(res.status, 200);
29
+ assert.strictEqual(data.status, 200);
30
+ assert.ok(Array.isArray(data.result));
31
+ assert.ok(data.result.length > 0, 'Should return at least one campaign');
32
+
33
+ const campaign = data.result[0];
34
+ assert.ok(campaign.name, 'Campaign should have name');
35
+ assert.ok(campaign.dealId, 'Campaign should have dealId');
36
+ assert.ok(campaign.mode, 'Campaign should have mode');
37
+ assert.ok(campaign.lineItem, 'Campaign should have lineItem');
38
+ assert.ok(campaign.publisher, 'Campaign should have publisher');
39
+ assert.ok(campaign.inventory, 'Campaign should have inventory');
40
+ assert.ok(Array.isArray(campaign.creatives), 'Campaign should have creatives array');
41
+ });
42
+
43
+ it('should return campaigns for valid device ID', async () => {
44
+ const res = await fetch(`${API_V2_DS}/deals/${TEST_DEVICE_ID}`);
45
+ const data = await res.json();
46
+
47
+ assert.strictEqual(res.status, 200);
48
+ assert.strictEqual(data.status, 200);
49
+ assert.ok(Array.isArray(data.result));
50
+ assert.ok(data.result.length > 0, 'Should return at least one campaign');
51
+ });
52
+
53
+ it('should return campaigns for valid reference ID', async () => {
54
+ const res = await fetch(`${API_V2_DS}/deals/${TEST_REFERENCE_ID}`);
55
+ const data = await res.json();
56
+
57
+ assert.strictEqual(res.status, 200);
58
+ assert.strictEqual(data.status, 200);
59
+ assert.ok(Array.isArray(data.result));
60
+ assert.ok(data.result.length > 0, 'Should return at least one campaign');
61
+ });
62
+
63
+ it('should include DIRECT specific fields for direct campaigns', async () => {
64
+ const res = await fetch(`${API_V2_DS}/deals/${TEST_INVENTORY_ID}`);
65
+ const data = await res.json();
66
+
67
+ const campaign = data.result.find(c => c.mode === 'direct');
68
+ if (campaign) {
69
+ assert.ok(campaign.direct, 'DIRECT campaign should have direct object');
70
+ assert.ok(campaign.deliveryTargeting, 'DIRECT campaign should have deliveryTargeting');
71
+ assert.ok(campaign.deliveryTargeting.ad_play_confirm_url, 'Should have ad_play_confirm_url');
72
+ }
73
+ });
74
+
75
+ it('should have correct inventory structure', async () => {
76
+ const res = await fetch(`${API_V2_DS}/deals/${TEST_INVENTORY_ID}`);
77
+ const data = await res.json();
78
+
79
+ assert.ok(data.result.length > 0, 'Should have results');
80
+ const campaign = data.result[0];
81
+ assert.ok(campaign.inventory.id, 'Inventory should have id');
82
+ assert.ok(campaign.inventory.deviceId, 'Inventory should have deviceId');
83
+ assert.ok(campaign.inventory.name, 'Inventory should have name');
84
+ });
85
+
86
+ it('should have correct publisher structure', async () => {
87
+ const res = await fetch(`${API_V2_DS}/deals/${TEST_INVENTORY_ID}`);
88
+ const data = await res.json();
89
+
90
+ assert.ok(data.result.length > 0, 'Should have results');
91
+ const campaign = data.result[0];
92
+ assert.ok(campaign.publisher.id, 'Publisher should have id');
93
+ assert.ok(campaign.publisher.name, 'Publisher should have name');
94
+ });
95
+
96
+ it('should have correct lineItem structure', async () => {
97
+ const res = await fetch(`${API_V2_DS}/deals/${TEST_INVENTORY_ID}`);
98
+ const data = await res.json();
99
+
100
+ assert.ok(data.result.length > 0, 'Should have results');
101
+ const campaign = data.result[0];
102
+ assert.ok(campaign.lineItem.id, 'LineItem should have id');
103
+ assert.ok(campaign.lineItem.name, 'LineItem should have name');
104
+ assert.ok(campaign.lineItem.creativeType, 'LineItem should have creativeType');
105
+ });
106
+
107
+ it('should have schedule and pacing data', async () => {
108
+ const res = await fetch(`${API_V2_DS}/deals/${TEST_INVENTORY_ID}`);
109
+ const data = await res.json();
110
+
111
+ assert.ok(data.result.length > 0, 'Should have results');
112
+ const campaign = data.result[0];
113
+ assert.ok(campaign.schedule, 'Campaign should have schedule');
114
+ assert.ok(campaign.pacing, 'Campaign should have pacing');
115
+ });
116
+
117
+ it('should exclude paused campaigns (is_playing = false)', async () => {
118
+ const res = await fetch(`${API_V2_DS}/deals/INV-INFLUENCE-000001`);
119
+ const data = await res.json();
120
+
121
+ assert.strictEqual(res.status, 200);
122
+ assert.strictEqual(data.result.length, 0, 'Should return empty array for paused campaigns');
123
+ });
124
+ });
125
+
126
+ describe('POST /confirm/:dealId/:lineItemId/:inventoryId', () => {
127
+ it('should return 404 for invalid confirmation', async () => {
128
+ const res = await fetch(
129
+ `${API_V2_DS}/confirm/invalid-deal/invalid-li/invalid-inv`,
130
+ { method: 'POST' }
131
+ );
132
+
133
+ assert.strictEqual(res.status, 404);
134
+ });
135
+
136
+ it('should return 204 for valid confirmation', async () => {
137
+ const res = await fetch(
138
+ `${API_V2_DS}/confirm/${TEST_DEAL_ID}/${TEST_LINE_ITEM_ID}/${TEST_INVENTORY_ID}`,
139
+ { method: 'POST' }
140
+ );
141
+
142
+ assert.strictEqual(res.status, 204);
143
+ });
144
+ });
145
+ });