001_initial_domain.sql 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. CREATE SCHEMA IF NOT EXISTS voc;
  2. CREATE TABLE IF NOT EXISTS voc.workspace (
  3. id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  4. public_id text NOT NULL UNIQUE,
  5. name text NOT NULL,
  6. case_name text NOT NULL,
  7. status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
  8. created_at timestamptz NOT NULL DEFAULT now(),
  9. updated_at timestamptz NOT NULL DEFAULT now()
  10. );
  11. CREATE TABLE IF NOT EXISTS voc.source_connection (
  12. id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  13. workspace_id bigint NOT NULL REFERENCES voc.workspace(id) ON DELETE CASCADE,
  14. platform text NOT NULL,
  15. connection_kind text NOT NULL DEFAULT 'fmode_gateway',
  16. status text NOT NULL DEFAULT 'configured' CHECK (status IN ('configured', 'active', 'disabled', 'error')),
  17. metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
  18. last_checked_at timestamptz,
  19. created_at timestamptz NOT NULL DEFAULT now(),
  20. updated_at timestamptz NOT NULL DEFAULT now(),
  21. UNIQUE (workspace_id, platform, connection_kind)
  22. );
  23. CREATE INDEX IF NOT EXISTS source_connection_workspace_idx
  24. ON voc.source_connection (workspace_id);
  25. CREATE TABLE IF NOT EXISTS voc.import_batch (
  26. id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  27. public_id text NOT NULL UNIQUE,
  28. workspace_id bigint NOT NULL REFERENCES voc.workspace(id) ON DELETE CASCADE,
  29. platform text NOT NULL,
  30. source_kind text NOT NULL,
  31. source_file text,
  32. source_hash text,
  33. sheet_names jsonb NOT NULL DEFAULT '[]'::jsonb,
  34. status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'partial', 'failed')),
  35. total_rows integer NOT NULL DEFAULT 0 CHECK (total_rows >= 0),
  36. success_rows integer NOT NULL DEFAULT 0 CHECK (success_rows >= 0),
  37. failed_rows integer NOT NULL DEFAULT 0 CHECK (failed_rows >= 0),
  38. started_at timestamptz,
  39. completed_at timestamptz,
  40. created_at timestamptz NOT NULL DEFAULT now()
  41. );
  42. CREATE INDEX IF NOT EXISTS import_batch_workspace_created_idx
  43. ON voc.import_batch (workspace_id, created_at DESC);
  44. CREATE TABLE IF NOT EXISTS voc.product (
  45. id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  46. workspace_id bigint NOT NULL REFERENCES voc.workspace(id) ON DELETE CASCADE,
  47. platform text NOT NULL,
  48. product_id text NOT NULL,
  49. product_key text NOT NULL,
  50. role text NOT NULL DEFAULT 'own' CHECK (role IN ('own', 'competitor')),
  51. brand text NOT NULL DEFAULT '',
  52. title text NOT NULL DEFAULT '',
  53. model text NOT NULL DEFAULT '',
  54. category_1 text NOT NULL DEFAULT '',
  55. category_2 text NOT NULL DEFAULT '',
  56. category_3 text NOT NULL DEFAULT '',
  57. source text NOT NULL DEFAULT '',
  58. raw_payload jsonb,
  59. created_at timestamptz NOT NULL DEFAULT now(),
  60. updated_at timestamptz NOT NULL DEFAULT now(),
  61. UNIQUE (workspace_id, platform, product_id),
  62. UNIQUE (workspace_id, product_key)
  63. );
  64. CREATE INDEX IF NOT EXISTS product_workspace_role_idx
  65. ON voc.product (workspace_id, role, id);
  66. CREATE INDEX IF NOT EXISTS product_workspace_category_idx
  67. ON voc.product (workspace_id, category_2, category_3);
  68. CREATE TABLE IF NOT EXISTS voc.daily_metric (
  69. id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  70. workspace_id bigint NOT NULL REFERENCES voc.workspace(id) ON DELETE CASCADE,
  71. product_id bigint NOT NULL REFERENCES voc.product(id) ON DELETE CASCADE,
  72. platform text NOT NULL,
  73. metric_date date NOT NULL,
  74. source text NOT NULL,
  75. gmv numeric(18, 2) NOT NULL DEFAULT 0,
  76. sold_units integer NOT NULL DEFAULT 0,
  77. transaction_orders integer NOT NULL DEFAULT 0,
  78. transaction_customers integer NOT NULL DEFAULT 0,
  79. impressions bigint NOT NULL DEFAULT 0,
  80. clicks bigint NOT NULL DEFAULT 0,
  81. views bigint NOT NULL DEFAULT 0,
  82. visitors bigint NOT NULL DEFAULT 0,
  83. cart_units integer NOT NULL DEFAULT 0,
  84. order_amount numeric(18, 2) NOT NULL DEFAULT 0,
  85. order_units integer NOT NULL DEFAULT 0,
  86. order_count integer NOT NULL DEFAULT 0,
  87. refund_amount numeric(18, 2) NOT NULL DEFAULT 0,
  88. refund_units integer NOT NULL DEFAULT 0,
  89. refund_orders integer NOT NULL DEFAULT 0,
  90. created_at timestamptz NOT NULL DEFAULT now(),
  91. updated_at timestamptz NOT NULL DEFAULT now(),
  92. UNIQUE (workspace_id, platform, product_id, metric_date, source)
  93. );
  94. CREATE INDEX IF NOT EXISTS daily_metric_product_date_idx
  95. ON voc.daily_metric (product_id, metric_date);
  96. CREATE INDEX IF NOT EXISTS daily_metric_workspace_date_idx
  97. ON voc.daily_metric (workspace_id, metric_date);
  98. CREATE TABLE IF NOT EXISTS voc.product_relation (
  99. id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  100. workspace_id bigint NOT NULL REFERENCES voc.workspace(id) ON DELETE CASCADE,
  101. platform text NOT NULL,
  102. own_product_id bigint NOT NULL REFERENCES voc.product(id) ON DELETE CASCADE,
  103. competitor_product_id bigint NOT NULL REFERENCES voc.product(id) ON DELETE CASCADE,
  104. relation_key text NOT NULL,
  105. category text NOT NULL DEFAULT '',
  106. created_at timestamptz NOT NULL DEFAULT now(),
  107. updated_at timestamptz NOT NULL DEFAULT now(),
  108. CHECK (own_product_id <> competitor_product_id),
  109. UNIQUE (workspace_id, platform, own_product_id, competitor_product_id),
  110. UNIQUE (workspace_id, relation_key)
  111. );
  112. CREATE INDEX IF NOT EXISTS product_relation_own_idx
  113. ON voc.product_relation (own_product_id, competitor_product_id);
  114. CREATE INDEX IF NOT EXISTS product_relation_competitor_idx
  115. ON voc.product_relation (competitor_product_id);
  116. CREATE INDEX IF NOT EXISTS product_relation_workspace_idx
  117. ON voc.product_relation (workspace_id);
  118. CREATE TABLE IF NOT EXISTS voc.review (
  119. id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  120. workspace_id bigint NOT NULL REFERENCES voc.workspace(id) ON DELETE CASCADE,
  121. product_id bigint NOT NULL REFERENCES voc.product(id) ON DELETE CASCADE,
  122. platform text NOT NULL,
  123. source_review_id text,
  124. review_key text NOT NULL,
  125. rating numeric(3, 2),
  126. content text NOT NULL,
  127. review_date timestamptz,
  128. raw_payload jsonb,
  129. created_at timestamptz NOT NULL DEFAULT now(),
  130. updated_at timestamptz NOT NULL DEFAULT now(),
  131. CHECK (rating IS NULL OR (rating >= 0 AND rating <= 5)),
  132. UNIQUE (workspace_id, platform, review_key)
  133. );
  134. CREATE INDEX IF NOT EXISTS review_product_date_idx
  135. ON voc.review (product_id, review_date DESC, id DESC);
  136. CREATE INDEX IF NOT EXISTS review_workspace_date_idx
  137. ON voc.review (workspace_id, review_date DESC, id DESC);
  138. CREATE TABLE IF NOT EXISTS voc.sync_job (
  139. id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  140. public_id text NOT NULL UNIQUE,
  141. workspace_id bigint NOT NULL REFERENCES voc.workspace(id) ON DELETE CASCADE,
  142. platform text NOT NULL,
  143. idempotency_key text NOT NULL,
  144. status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'partial', 'failed', 'cancelled')),
  145. scopes jsonb NOT NULL DEFAULT '[]'::jsonb,
  146. product_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
  147. progress integer NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100),
  148. attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
  149. max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts >= 1),
  150. worker_id text,
  151. error_summary text,
  152. requested_at timestamptz NOT NULL DEFAULT now(),
  153. started_at timestamptz,
  154. completed_at timestamptz,
  155. updated_at timestamptz NOT NULL DEFAULT now(),
  156. UNIQUE (workspace_id, idempotency_key)
  157. );
  158. CREATE INDEX IF NOT EXISTS sync_job_workspace_requested_idx
  159. ON voc.sync_job (workspace_id, requested_at DESC);
  160. CREATE INDEX IF NOT EXISTS sync_job_pending_idx
  161. ON voc.sync_job (requested_at, id)
  162. WHERE status = 'pending';
  163. CREATE TABLE IF NOT EXISTS voc.sync_job_event (
  164. id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  165. sync_job_id bigint NOT NULL REFERENCES voc.sync_job(id) ON DELETE CASCADE,
  166. level text NOT NULL DEFAULT 'info' CHECK (level IN ('debug', 'info', 'warning', 'error')),
  167. event_type text NOT NULL,
  168. message text NOT NULL,
  169. details jsonb NOT NULL DEFAULT '{}'::jsonb,
  170. created_at timestamptz NOT NULL DEFAULT now()
  171. );
  172. CREATE INDEX IF NOT EXISTS sync_job_event_job_created_idx
  173. ON voc.sync_job_event (sync_job_id, created_at, id);
  174. INSERT INTO voc.workspace (public_id, name, case_name)
  175. VALUES ('demashi', 'Demashi JD VOC', 'Demashi')
  176. ON CONFLICT (public_id) DO NOTHING;
  177. INSERT INTO voc.source_connection (workspace_id, platform, connection_kind, status, metadata)
  178. SELECT id, 'jd', 'fmode_gateway', 'configured', '{"credentialStorage":"environment"}'::jsonb
  179. FROM voc.workspace
  180. WHERE public_id = 'demashi'
  181. ON CONFLICT (workspace_id, platform, connection_kind) DO NOTHING;