@lodashventure/medusa-campaign 1.4.1 → 1.4.2

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 (46) hide show
  1. package/.medusa/server/src/admin/index.js +939 -504
  2. package/.medusa/server/src/admin/index.mjs +941 -506
  3. package/.medusa/server/src/api/admin/buy-x-get-y/[id]/route.js +2 -6
  4. package/.medusa/server/src/api/admin/coupons/[id]/route.js +76 -0
  5. package/.medusa/server/src/api/admin/coupons/route.js +88 -0
  6. package/.medusa/server/src/api/middlewares.js +32 -1
  7. package/.medusa/server/src/api/store/campaigns/route.js +78 -7
  8. package/.medusa/server/src/api/store/coupons/public/route.js +110 -0
  9. package/.medusa/server/src/api/store/customers/me/coupons/route.js +148 -0
  10. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +2 -2
  11. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +2 -2
  12. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251101000000.js +16 -0
  13. package/.medusa/server/src/modules/custom-campaigns/types/campaign-type.enum.js +2 -1
  14. package/.medusa/server/src/workflows/custom-campaign/createCouponCampaignWorkflow.js +105 -0
  15. package/.medusa/server/src/workflows/custom-campaign/updateCouponCampaignWorkflow.js +59 -0
  16. package/.medusa/server/src/workflows/index.js +6 -2
  17. package/package.json +15 -30
  18. package/src/admin/components/BuyXGetYForm.tsx +24 -13
  19. package/src/admin/components/CouponForm.tsx +352 -0
  20. package/src/admin/components/CouponPage.tsx +104 -0
  21. package/src/admin/components/ProductSelector.tsx +22 -11
  22. package/src/admin/hooks/useCouponById.ts +36 -0
  23. package/src/admin/hooks/useCoupons.ts +46 -0
  24. package/src/admin/hooks/useFlashSaleById.ts +36 -10
  25. package/src/admin/hooks/useFlashSales.ts +36 -10
  26. package/src/admin/routes/coupons/[id]/page.tsx +147 -0
  27. package/src/admin/routes/coupons/create/page.tsx +49 -0
  28. package/src/admin/routes/coupons/page.tsx +15 -0
  29. package/src/admin/routes/flash-sales/[id]/page.tsx +2 -11
  30. package/src/admin/routes/flash-sales/create/page.tsx +0 -6
  31. package/src/admin/widgets/campaign-detail-widget.tsx +33 -26
  32. package/src/api/admin/buy-x-get-y/[id]/route.ts +11 -15
  33. package/src/api/admin/coupons/[id]/route.ts +98 -0
  34. package/src/api/admin/coupons/route.ts +109 -0
  35. package/src/api/middlewares.ts +34 -0
  36. package/src/api/store/campaigns/route.ts +107 -24
  37. package/src/api/store/coupons/public/route.ts +165 -0
  38. package/src/api/store/customers/me/coupons/route.ts +244 -0
  39. package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +1 -1
  40. package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +1 -1
  41. package/src/modules/custom-campaigns/migrations/Migration20251101000000.ts +21 -0
  42. package/src/modules/custom-campaigns/types/campaign-type.enum.ts +1 -0
  43. package/src/workflows/custom-campaign/createCouponCampaignWorkflow.ts +176 -0
  44. package/src/workflows/custom-campaign/updateCouponCampaignWorkflow.ts +105 -0
  45. package/src/workflows/index.ts +3 -1
  46. package/src/admin/widgets/campaign-stats-widget.tsx +0 -238
@@ -1,15 +1,26 @@
1
- import { CampaignDTO } from "@medusajs/framework/types";
2
- import { useQuery } from "@tanstack/react-query";
3
1
  import axios from "axios";
2
+ import { useEffect, useState } from "react";
3
+
4
+ type CampaignDTO = any;
4
5
 
5
6
  export const useFlashSales = (pagination: {
6
7
  limit: number;
7
8
  offset: number;
8
9
  }) => {
9
- const query = useQuery({
10
- queryKey: ["flash-sales", pagination],
11
- queryFn: async () => {
12
- const { data } = await axios.get<{
10
+ const [data, setData] = useState<{
11
+ campaigns: CampaignDTO[];
12
+ count: number;
13
+ limit: number;
14
+ offset: number;
15
+ } | null>(null);
16
+ const [isLoading, setIsLoading] = useState(true);
17
+ const [error, setError] = useState<Error | null>(null);
18
+
19
+ const fetchFlashSales = async () => {
20
+ setIsLoading(true);
21
+ setError(null);
22
+ try {
23
+ const response = await axios.get<{
13
24
  campaigns: CampaignDTO[];
14
25
  count: number;
15
26
  limit: number;
@@ -17,9 +28,24 @@ export const useFlashSales = (pagination: {
17
28
  }>("/admin/flash-sales", {
18
29
  params: pagination,
19
30
  });
20
- return data;
21
- },
22
- });
31
+ setData(response.data);
32
+ } catch (err) {
33
+ setError(
34
+ err instanceof Error ? err : new Error("Failed to fetch flash sales"),
35
+ );
36
+ } finally {
37
+ setIsLoading(false);
38
+ }
39
+ };
40
+
41
+ useEffect(() => {
42
+ fetchFlashSales();
43
+ }, [pagination.limit, pagination.offset]);
23
44
 
24
- return query;
45
+ return {
46
+ data,
47
+ isLoading,
48
+ error,
49
+ refetch: fetchFlashSales,
50
+ };
25
51
  };
@@ -0,0 +1,147 @@
1
+ import { defineRouteConfig } from "@medusajs/admin-sdk";
2
+ import { Tag } from "@medusajs/icons";
3
+ import {
4
+ Button,
5
+ Container,
6
+ Tabs,
7
+ toast,
8
+ } from "@medusajs/ui";
9
+ import axios from "axios";
10
+ import dayjs from "dayjs";
11
+ import { FC, useState } from "react";
12
+ import { useNavigate, useParams } from "react-router-dom";
13
+ import {
14
+ CouponForm,
15
+ CouponFormData,
16
+ } from "../../../components/CouponForm";
17
+ import { useCouponById } from "../../../hooks/useCouponById";
18
+ import { CampaignDetailForm } from "../../../components/campaign-detail-form";
19
+
20
+ const CouponDetail: FC = () => {
21
+ const { id } = useParams<{ id: string }>();
22
+ const navigate = useNavigate();
23
+ const [isEditing, setIsEditing] = useState(false);
24
+
25
+ const { data, isLoading, refetch } = useCouponById(id ?? "");
26
+
27
+ if (isLoading) {
28
+ return (
29
+ <Container className="flex items-center justify-center h-screen">
30
+ <div className="animate-spin h-10 w-10 border-4 border-primary rounded-full border-t-transparent"></div>
31
+ </Container>
32
+ );
33
+ }
34
+
35
+ if (!data?.campaign) {
36
+ return (
37
+ <Container>
38
+ <p>Coupon not found</p>
39
+ </Container>
40
+ );
41
+ }
42
+
43
+ const campaign = data.campaign;
44
+ const promotion = campaign.promotions?.[0];
45
+
46
+ const initialValues: Partial<CouponFormData> = {
47
+ name: campaign.name ?? "",
48
+ description: campaign.description ?? "",
49
+ type: "coupon",
50
+ code: promotion?.code ?? "",
51
+ discount_type: (promotion?.application_method?.type as CouponFormData["discount_type"]) ?? "percentage",
52
+ discount_value: Number(promotion?.application_method?.value ?? 0),
53
+ currency_code: promotion?.application_method?.currency_code ?? undefined,
54
+ starts_at: campaign.starts_at
55
+ ? dayjs(campaign.starts_at).format("YYYY-MM-DDTHH:mm")
56
+ : undefined,
57
+ ends_at: campaign.ends_at
58
+ ? dayjs(campaign.ends_at).format("YYYY-MM-DDTHH:mm")
59
+ : undefined,
60
+ allocation: promotion?.application_method?.allocation ?? "total",
61
+ target_type: promotion?.application_method?.target_type ?? "order",
62
+ };
63
+
64
+ const handleSubmit = async (formData: CouponFormData) => {
65
+ if (!id) {
66
+ return;
67
+ }
68
+
69
+ try {
70
+ await axios.put(`/admin/coupons/${id}`, {
71
+ ...formData,
72
+ starts_at: new Date(formData.starts_at).toUTCString(),
73
+ ends_at: new Date(formData.ends_at).toUTCString(),
74
+ currency_code:
75
+ formData.discount_type === "fixed"
76
+ ? formData.currency_code
77
+ : undefined,
78
+ });
79
+
80
+ toast.success("Coupon updated successfully");
81
+ setIsEditing(false);
82
+ refetch();
83
+ } catch (error) {
84
+ let message = "Failed to update coupon";
85
+ if (axios.isAxiosError(error)) {
86
+ message = error.response?.data?.message ?? message;
87
+ } else if (error instanceof Error) {
88
+ message = error.message;
89
+ }
90
+
91
+ toast.error("Failed to update coupon", {
92
+ description: message,
93
+ });
94
+ }
95
+ };
96
+
97
+ return (
98
+ <Container>
99
+ <div className="mb-6 flex items-center justify-between">
100
+ <div>
101
+ <h1 className="text-2xl font-semibold mb-1">{campaign.name}</h1>
102
+ <p className="text-ui-fg-subtle">
103
+ Manage coupon configuration and content
104
+ </p>
105
+ </div>
106
+ <div className="flex gap-2">
107
+ <Button variant="secondary" onClick={() => navigate("/coupons")}>
108
+ Back to Coupons
109
+ </Button>
110
+ <Button onClick={() => setIsEditing((prev) => !prev)}>
111
+ {isEditing ? "View Mode" : "Edit Mode"}
112
+ </Button>
113
+ </div>
114
+ </div>
115
+
116
+ <Tabs defaultValue="settings">
117
+ <Tabs.List>
118
+ <Tabs.Trigger value="settings">Coupon Settings</Tabs.Trigger>
119
+ <Tabs.Trigger value="details">Coupon Details</Tabs.Trigger>
120
+ </Tabs.List>
121
+
122
+ <Tabs.Content value="settings" className="mt-6">
123
+ <CouponForm
124
+ initialData={initialValues}
125
+ onSubmit={handleSubmit}
126
+ onCancel={() => navigate("/coupons")}
127
+ disabled={!isEditing}
128
+ />
129
+ </Tabs.Content>
130
+
131
+ <Tabs.Content value="details" className="mt-6">
132
+ <CampaignDetailForm
133
+ campaignId={campaign.id}
134
+ campaignName={campaign.name ?? ""}
135
+ />
136
+ </Tabs.Content>
137
+ </Tabs>
138
+ </Container>
139
+ );
140
+ };
141
+
142
+ export const config = defineRouteConfig({
143
+ label: "Coupon Detail",
144
+ icon: Tag,
145
+ });
146
+
147
+ export default CouponDetail;
@@ -0,0 +1,49 @@
1
+ import { toast } from "@medusajs/ui";
2
+ import axios from "axios";
3
+ import { FC } from "react";
4
+ import { useNavigate } from "react-router-dom";
5
+ import {
6
+ CouponForm,
7
+ CouponFormData,
8
+ } from "../../../components/CouponForm";
9
+
10
+ const CouponCreate: FC = () => {
11
+ const navigate = useNavigate();
12
+
13
+ const handleSubmit = async (formData: CouponFormData) => {
14
+ try {
15
+ await axios.post("/admin/coupons", {
16
+ ...formData,
17
+ starts_at: new Date(formData.starts_at).toUTCString(),
18
+ ends_at: new Date(formData.ends_at).toUTCString(),
19
+ currency_code:
20
+ formData.discount_type === "fixed"
21
+ ? formData.currency_code
22
+ : undefined,
23
+ });
24
+
25
+ toast.success("Coupon created successfully");
26
+ navigate("/coupons");
27
+ } catch (error) {
28
+ let message = "Failed to create coupon";
29
+ if (axios.isAxiosError(error)) {
30
+ message = error.response?.data?.message ?? message;
31
+ } else if (error instanceof Error) {
32
+ message = error.message;
33
+ }
34
+
35
+ toast.error("Failed to create coupon", {
36
+ description: message,
37
+ });
38
+ }
39
+ };
40
+
41
+ return (
42
+ <CouponForm
43
+ onSubmit={handleSubmit}
44
+ onCancel={() => navigate("/coupons")}
45
+ />
46
+ );
47
+ };
48
+
49
+ export default CouponCreate;
@@ -0,0 +1,15 @@
1
+ import { defineRouteConfig } from "@medusajs/admin-sdk";
2
+ import { Tag } from "@medusajs/icons";
3
+ import { FC } from "react";
4
+ import { CouponPage } from "../../components/CouponPage";
5
+
6
+ const Coupons: FC = () => {
7
+ return <CouponPage />;
8
+ };
9
+
10
+ export const config = defineRouteConfig({
11
+ label: "Coupons",
12
+ icon: Tag,
13
+ });
14
+
15
+ export default Coupons;
@@ -1,7 +1,6 @@
1
1
  import { defineRouteConfig } from "@medusajs/admin-sdk";
2
2
  import { Sparkles } from "@medusajs/icons";
3
3
  import { Button, Container, toast, Tabs } from "@medusajs/ui";
4
- import { useQueryClient } from "@tanstack/react-query";
5
4
  import axios from "axios";
6
5
  import { FC, useState } from "react";
7
6
  import { useNavigate, useParams } from "react-router-dom";
@@ -18,8 +17,7 @@ import { CampaignDetailForm } from "../../../components/campaign-detail-form";
18
17
  const FlashSaleDetail: FC = () => {
19
18
  const { id } = useParams<{ id: string }>();
20
19
  const navigate = useNavigate();
21
- const queryClient = useQueryClient();
22
- const { data, isLoading } = useFlashSaleById(id || "");
20
+ const { data, isLoading, refetch } = useFlashSaleById(id || "");
23
21
 
24
22
  const [isEditing, setIsEditing] = useState(false);
25
23
 
@@ -32,14 +30,7 @@ const FlashSaleDetail: FC = () => {
32
30
  });
33
31
 
34
32
  toast.success("Flash sale updated successfully");
35
- queryClient.invalidateQueries({
36
- exact: false,
37
- predicate(query) {
38
- return ["flash-sales", "flash-sale"].includes(
39
- query.queryKey[0] as string,
40
- );
41
- },
42
- });
33
+ refetch();
43
34
  navigate("/flash-sales");
44
35
  } catch (error) {
45
36
  let message: string;
@@ -1,5 +1,4 @@
1
1
  import { toast } from "@medusajs/ui";
2
- import { useQueryClient } from "@tanstack/react-query";
3
2
  import axios from "axios";
4
3
  import { FC } from "react";
5
4
  import { useNavigate } from "react-router-dom";
@@ -7,7 +6,6 @@ import { CampaignData, FlashSaleForm } from "../../../components/FlashSaleForm";
7
6
 
8
7
  const FlashSaleCreate: FC = () => {
9
8
  const navigate = useNavigate();
10
- const queryClient = useQueryClient();
11
9
 
12
10
  async function handleSubmit(campaignData: CampaignData) {
13
11
  try {
@@ -17,10 +15,6 @@ const FlashSaleCreate: FC = () => {
17
15
  ends_at: new Date(campaignData.ends_at).toUTCString(),
18
16
  });
19
17
  toast.success("Flash sale created successfully");
20
- queryClient.invalidateQueries({
21
- exact: false,
22
- queryKey: ["flash-sales"],
23
- });
24
18
  navigate("/flash-sales");
25
19
  } catch (error) {
26
20
  let message: string;
@@ -1,12 +1,5 @@
1
1
  import { defineWidgetConfig } from "@medusajs/admin-sdk";
2
- import {
3
- Container,
4
- Heading,
5
- Text,
6
- Badge,
7
- Button,
8
- Alert,
9
- } from "@medusajs/ui";
2
+ import { Container, Heading, Text, Badge, Button, Alert } from "@medusajs/ui";
10
3
  import { Sparkles, PhotoSolid, PencilSquare, Eye } from "@medusajs/icons";
11
4
  import { useState, useEffect } from "react";
12
5
  import { useNavigate } from "react-router-dom";
@@ -38,10 +31,14 @@ interface CampaignDetailWidgetProps {
38
31
  data: any; // Promotion data
39
32
  }
40
33
 
41
- const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) => {
34
+ const CampaignDetailWidget = ({
35
+ data: promotion,
36
+ }: CampaignDetailWidgetProps) => {
42
37
  const navigate = useNavigate();
43
38
  const [campaign, setCampaign] = useState<Campaign | null>(null);
44
- const [campaignDetail, setCampaignDetail] = useState<CampaignDetail | null>(null);
39
+ const [campaignDetail, setCampaignDetail] = useState<CampaignDetail | null>(
40
+ null,
41
+ );
45
42
  const [loading, setLoading] = useState(true);
46
43
  const [error, setError] = useState<string | null>(null);
47
44
 
@@ -65,7 +62,7 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
65
62
  `/admin/flash-sales/${promotion.campaign_id}`,
66
63
  {
67
64
  credentials: "include",
68
- }
65
+ },
69
66
  );
70
67
 
71
68
  if (campaignResponse.ok) {
@@ -149,7 +146,8 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
149
146
 
150
147
  const hasCampaignDetail = !!campaignDetail;
151
148
  const hasImages = campaignDetail?.image_url || campaignDetail?.thumbnail_url;
152
- const hasContent = campaignDetail?.detail_content || campaignDetail?.terms_and_conditions;
149
+ const hasContent =
150
+ campaignDetail?.detail_content || campaignDetail?.terms_and_conditions;
153
151
  const hasSEO = campaignDetail?.meta_title || campaignDetail?.meta_description;
154
152
 
155
153
  return (
@@ -164,7 +162,9 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
164
162
  <div className="mb-4 p-4 rounded-lg border bg-ui-bg-subtle">
165
163
  <div className="flex items-start justify-between mb-3">
166
164
  <div className="flex-1">
167
- <Text className="font-semibold text-lg mb-1">{campaign.name}</Text>
165
+ <Text className="font-semibold text-lg mb-1">
166
+ {campaign.name}
167
+ </Text>
168
168
  {campaign.description && (
169
169
  <Text className="text-sm text-ui-fg-subtle line-clamp-2">
170
170
  {campaign.description}
@@ -180,15 +180,19 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
180
180
  {/* Campaign Detail Status */}
181
181
  <div className="grid grid-cols-2 gap-2 mt-3 pt-3 border-t">
182
182
  <div className="flex items-center gap-2">
183
- <PhotoSolid className={`h-4 w-4 ${hasImages ? 'text-green-500' : 'text-ui-fg-muted'}`} />
183
+ <PhotoSolid
184
+ className={`h-4 w-4 ${hasImages ? "text-green-500" : "text-ui-fg-muted"}`}
185
+ />
184
186
  <Text className="text-xs">
185
- {hasImages ? 'Images added' : 'No images'}
187
+ {hasImages ? "Images added" : "No images"}
186
188
  </Text>
187
189
  </div>
188
190
  <div className="flex items-center gap-2">
189
- <PencilSquare className={`h-4 w-4 ${hasContent ? 'text-green-500' : 'text-ui-fg-muted'}`} />
191
+ <PencilSquare
192
+ className={`h-4 w-4 ${hasContent ? "text-green-500" : "text-ui-fg-muted"}`}
193
+ />
190
194
  <Text className="text-xs">
191
- {hasContent ? 'Content added' : 'No content'}
195
+ {hasContent ? "Content added" : "No content"}
192
196
  </Text>
193
197
  </div>
194
198
  </div>
@@ -223,7 +227,9 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
223
227
 
224
228
  {campaignDetail.link_url && (
225
229
  <div className="flex items-center gap-2 text-xs">
226
- <Badge size="xsmall" color="blue">CTA Link</Badge>
230
+ <Badge size="xsmall" color="blue">
231
+ CTA Link
232
+ </Badge>
227
233
  <Text className="text-ui-fg-subtle truncate">
228
234
  {campaignDetail.link_text || campaignDetail.link_url}
229
235
  </Text>
@@ -232,7 +238,9 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
232
238
 
233
239
  {hasSEO && (
234
240
  <div className="flex items-center gap-2 text-xs">
235
- <Badge size="xsmall" color="green">SEO</Badge>
241
+ <Badge size="xsmall" color="green">
242
+ SEO
243
+ </Badge>
236
244
  <Text className="text-ui-fg-subtle">
237
245
  Meta fields configured
238
246
  </Text>
@@ -245,18 +253,15 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
245
253
  {!hasCampaignDetail && (
246
254
  <Alert variant="info" className="mb-4">
247
255
  <Text className="text-sm">
248
- No campaign details added yet. Add images, content, and SEO to enhance this campaign.
256
+ No campaign details added yet. Add images, content, and SEO to
257
+ enhance this campaign.
249
258
  </Text>
250
259
  </Alert>
251
260
  )}
252
261
 
253
262
  {/* Actions */}
254
263
  <div className="flex gap-2">
255
- <Button
256
- variant="secondary"
257
- size="small"
258
- onClick={handleEditCampaign}
259
- >
264
+ <Button variant="secondary" size="small" onClick={handleEditCampaign}>
260
265
  <PencilSquare className="mr-1" />
261
266
  Edit Campaign
262
267
  </Button>
@@ -277,7 +282,9 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
277
282
  <div className="mt-4 pt-4 border-t grid grid-cols-2 gap-3 text-xs">
278
283
  <div>
279
284
  <Text className="text-ui-fg-subtle">Display Order</Text>
280
- <Text className="font-medium">{campaignDetail.display_order}</Text>
285
+ <Text className="font-medium">
286
+ {campaignDetail.display_order}
287
+ </Text>
281
288
  </div>
282
289
  <div>
283
290
  <Text className="text-ui-fg-subtle">Status</Text>
@@ -19,7 +19,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
19
19
  if (!id) {
20
20
  throw new MedusaError(
21
21
  MedusaError.Types.INVALID_DATA,
22
- "Campaign ID is required"
22
+ "Campaign ID is required",
23
23
  );
24
24
  }
25
25
 
@@ -34,11 +34,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
34
34
  data: [customCampaignTypes],
35
35
  } = await query.graph({
36
36
  entity: "custom_campaign_type",
37
- fields: [
38
- "id",
39
- "campaign.*",
40
- "campaign.promotions.*",
41
- ],
37
+ fields: ["id", "campaign.*", "campaign.promotions.*"],
42
38
  filters: {
43
39
  type: CampaignTypeEnum.BuyXGetY,
44
40
  campaign_id: id,
@@ -50,7 +46,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
50
46
  if (!campaign) {
51
47
  throw new MedusaError(
52
48
  MedusaError.Types.NOT_FOUND,
53
- `Buy X Get Y campaign with ID ${id} not found`
49
+ `Buy X Get Y campaign with ID ${id} not found`,
54
50
  );
55
51
  }
56
52
 
@@ -64,10 +60,10 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
64
60
  const rules: BuyXGetYCampaign["rules"] = [];
65
61
  for (const config of buyXGetYConfigs) {
66
62
  const triggerProduct = await productService.retrieveProduct(
67
- config.trigger_product_id
63
+ config.trigger_product_id,
68
64
  );
69
65
  const rewardProduct = await productService.retrieveProduct(
70
- config.reward_product_id
66
+ config.reward_product_id,
71
67
  );
72
68
 
73
69
  rules.push({
@@ -100,7 +96,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
100
96
  console.error("Error fetching Buy X Get Y campaign:", error);
101
97
  throw new MedusaError(
102
98
  MedusaError.Types.UNEXPECTED_STATE,
103
- "An error occurred while fetching the Buy X Get Y campaign"
99
+ "An error occurred while fetching the Buy X Get Y campaign",
104
100
  );
105
101
  }
106
102
  };
@@ -110,7 +106,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
110
106
  */
111
107
  export const PUT = async (
112
108
  req: MedusaRequest<BuyXGetYCampaign>,
113
- res: MedusaResponse
109
+ res: MedusaResponse,
114
110
  ) => {
115
111
  const { id } = req.params;
116
112
  const body = req.body;
@@ -118,7 +114,7 @@ export const PUT = async (
118
114
  if (!id) {
119
115
  throw new MedusaError(
120
116
  MedusaError.Types.INVALID_DATA,
121
- "Campaign ID is required"
117
+ "Campaign ID is required",
122
118
  );
123
119
  }
124
120
 
@@ -127,7 +123,7 @@ export const PUT = async (
127
123
  if (new Date(body.ends_at) < new Date(body.starts_at)) {
128
124
  throw new MedusaError(
129
125
  MedusaError.Types.INVALID_DATA,
130
- "End date must be after start date"
126
+ "End date must be after start date",
131
127
  );
132
128
  }
133
129
 
@@ -136,7 +132,7 @@ export const PUT = async (
136
132
  if (rule.rewardType !== "free" && !rule.rewardValue) {
137
133
  throw new MedusaError(
138
134
  MedusaError.Types.INVALID_DATA,
139
- "Reward value is required for percentage and fixed reward types"
135
+ "Reward value is required for percentage and fixed reward types",
140
136
  );
141
137
  }
142
138
  }
@@ -158,7 +154,7 @@ export const PUT = async (
158
154
  console.error("Error updating Buy X Get Y campaign:", error);
159
155
  throw new MedusaError(
160
156
  MedusaError.Types.UNEXPECTED_STATE,
161
- "An error occurred while updating the Buy X Get Y campaign"
157
+ "An error occurred while updating the Buy X Get Y campaign",
162
158
  );
163
159
  }
164
160
  };
@@ -0,0 +1,98 @@
1
+ import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
2
+ import { MedusaError, Modules } from "@medusajs/framework/utils";
3
+ import z from "zod";
4
+ import { createCouponCampaignSchema } from "../route";
5
+ import { updateCouponCampaignWorkflow } from "../../../../workflows/custom-campaign/updateCouponCampaignWorkflow";
6
+ import { CUSTOM_CAMPAIGN_MODULE } from "../../../../modules/custom-campaigns";
7
+ import CustomCampaignModuleService from "../../../../modules/custom-campaigns/service";
8
+ import { CampaignTypeEnum } from "../../../../modules/custom-campaigns/types/campaign-type.enum";
9
+
10
+ const updateCouponSchema = createCouponCampaignSchema.extend({
11
+ type: z.literal(CampaignTypeEnum.Coupon),
12
+ });
13
+
14
+ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
15
+ const { id } = req.params;
16
+
17
+ const promotionService = container.resolve(Modules.PROMOTION);
18
+ const customCampaignModuleService =
19
+ container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
20
+
21
+ try {
22
+ const campaign = await promotionService.retrieveCampaign(id, {
23
+ relations: ["promotions", "promotions.application_method"],
24
+ });
25
+
26
+ if (!campaign) {
27
+ throw new MedusaError(
28
+ MedusaError.Types.NOT_FOUND,
29
+ "Coupon campaign not found",
30
+ );
31
+ }
32
+
33
+ const [campaignType] =
34
+ await customCampaignModuleService.listCustomCampaignTypes({
35
+ campaign_id: campaign.id,
36
+ });
37
+
38
+ if (campaignType?.type !== CampaignTypeEnum.Coupon) {
39
+ throw new MedusaError(
40
+ MedusaError.Types.INVALID_DATA,
41
+ "Campaign is not a coupon",
42
+ );
43
+ }
44
+
45
+ const [detail] = await customCampaignModuleService.listCampaignDetails({
46
+ campaign_id: campaign.id,
47
+ });
48
+
49
+ res.status(200).json({
50
+ campaign,
51
+ campaign_detail: detail ?? null,
52
+ });
53
+ } catch (error) {
54
+ if (error instanceof MedusaError) {
55
+ throw error;
56
+ }
57
+
58
+ throw new MedusaError(
59
+ MedusaError.Types.INVALID_DATA,
60
+ (error as Error)?.message ?? "Failed to retrieve coupon",
61
+ );
62
+ }
63
+ };
64
+
65
+ export const PUT = async (
66
+ req: MedusaRequest<z.infer<typeof updateCouponSchema>>,
67
+ res: MedusaResponse,
68
+ ) => {
69
+ const { id } = req.params;
70
+ const body = req.body;
71
+
72
+ if (body.type !== CampaignTypeEnum.Coupon) {
73
+ throw new MedusaError(
74
+ MedusaError.Types.INVALID_DATA,
75
+ "Campaign type must be coupon",
76
+ );
77
+ }
78
+
79
+ await updateCouponCampaignWorkflow.run({
80
+ input: {
81
+ id,
82
+ name: body.name,
83
+ description: body.description,
84
+ code: body.code,
85
+ starts_at: new Date(body.starts_at),
86
+ ends_at: new Date(body.ends_at),
87
+ discount_type: body.discount_type,
88
+ discount_value: body.discount_value,
89
+ currency_code: body.currency_code,
90
+ allocation: body.allocation,
91
+ target_type: body.target_type,
92
+ },
93
+ });
94
+
95
+ res.status(200).json({
96
+ message: "Coupon campaign updated",
97
+ });
98
+ };