Skip to content

Commit

Permalink
Add Stripe Tax, Paid Shipping, and Cancel Order (#510)
Browse files Browse the repository at this point in the history
* Stripe Integration: Add Tax

* add another shipping option

* basic cancel functionality

* add missing CancelOrderModal file

* fix static checks failures

* fix lint errors

* only allow modify order before shipping, and cancel reason dropdown menu

* address Ben's comments and revert back to use production productId

---------

Co-authored-by: Mia Ngo <[email protected]>
  • Loading branch information
han-ngo and Mia Ngo authored Oct 29, 2024
1 parent a5adfa7 commit 488dd95
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 1 deletion.
167 changes: 167 additions & 0 deletions frontend/src/components/modals/CancelOrderModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useState } from "react";

import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { paths } from "@/gen/api";
import { useAlertQueue } from "@/hooks/useAlertQueue";
import { useAuthentication } from "@/hooks/useAuth";

type Order =
paths["/orders/get_user_orders"]["get"]["responses"][200]["content"]["application/json"][0];

interface CancelOrderModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
order: Order;
onOrderUpdate: (updatedOrder: Order) => void;
}

const CancelOrderModal: React.FC<CancelOrderModalProps> = ({
isOpen,
onOpenChange,
order,
onOrderUpdate,
}) => {
const { client } = useAuthentication();
const { addAlert, addErrorAlert } = useAlertQueue();

const cancellationReasons = [
"Found a better price",
"Change of mind",
"Item no longer needed",
"Ordered by mistake",
"Other",
];

const MAX_REASON_LENGTH = 500;

const [cancellation, setCancellation] = useState({
payment_intent_id: order["stripe_payment_intent_id"],
cancel_reason: { reason: "", details: "" },
amount: order["amount"],
});
const [customReason, setCustomReason] = useState("");

const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { value } = e.target;
setCancellation((prev) => ({
...prev,
cancel_reason: {
reason: value,
details: value === "Other" ? customReason : "",
},
}));
};

const handleCustomReasonChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let { value } = e.target;

// Limit the input length
if (value.length > MAX_REASON_LENGTH) {
value = value.slice(0, MAX_REASON_LENGTH);
}

setCustomReason(value);
setCancellation((prev) => ({
...prev,
cancel_reason: {
...prev.cancel_reason,
details: value,
},
}));
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const { data, error } = await client.PUT("/stripe/refunds/{order_id}", {
params: { path: { order_id: order.id } },
body: cancellation,
});

if (error) {
addErrorAlert("Failed to cancel the order");
console.error("Error canceling order:", error);
} else {
addAlert("Order successfully canceled", "success");
onOrderUpdate(data);
}

onOpenChange(false);
} catch (error) {
console.error("Error canceling order:", error);
}
};

return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px] bg-gray-1 text-gray-12 border border-gray-3 rounded-lg shadow-lg">
<DialogHeader>
<DialogTitle>Cancel Order</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="cancel_reason">Cancel Reason</Label>
<select
id="cancel_reason"
name="cancel_reason"
value={cancellation.cancel_reason.reason}
onChange={handleSelectChange}
className="bg-gray-2 border-gray-3 text-gray-12 rounded-md p-2"
>
<option value="" disabled>
Select a reason for cancellation
</option>
{cancellationReasons.map((reason) => (
<option key={reason} value={reason}>
{reason}
</option>
))}
</select>
{cancellation.cancel_reason.reason === "Other" && (
<div className="mt-2">
<input
type="text"
value={customReason}
onChange={handleCustomReasonChange}
placeholder="Please specify"
maxLength={MAX_REASON_LENGTH}
className="bg-gray-2 border-gray-3 text-gray-12 rounded-md p-2 w-full"
/>
<p className="text-sm text-gray-500 mt-1">
{MAX_REASON_LENGTH - customReason.length} characters
remaining
</p>
</div>
)}
</div>
</div>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button
type="submit"
className="bg-primary-9 text-gray-1 hover:bg-gray-12"
>
Save Changes
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
};

export default CancelOrderModal;
21 changes: 21 additions & 0 deletions frontend/src/components/orders/OrderCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState } from "react";

import CancelOrderModal from "@/components/modals/CancelOrderModal";
import EditAddressModal from "@/components/modals/EditAddressModal";
import {
DropdownMenu,
Expand Down Expand Up @@ -35,6 +36,7 @@ const activeStatuses = [
"shipped",
];
const redStatuses = ["cancelled", "refunded", "failed"];
const canModifyStatuses = ["processing", "in_development", "being_assembled"];

const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({
orderWithProduct: initialOrderWithProduct,
Expand All @@ -44,6 +46,7 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({
);
const { order, product } = orderWithProduct;
const [isEditAddressModalOpen, setIsEditAddressModalOpen] = useState(false);
const [isCancelOrderModalOpen, setIsCancelOrderModalOpen] = useState(false);

const currentStatusIndex = orderStatuses.indexOf(order.status);
const isRedStatus = redStatuses.includes(order.status);
Expand All @@ -69,6 +72,10 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({
setOrderWithProduct((prev) => ({ ...prev, order: updatedOrder }));
};

const canModifyOrder = () => {
return canModifyStatuses.includes(order.status);
};

return (
<div className="bg-white shadow-md rounded-lg p-4 md:p-6 mb-4 w-full">
<h2 className="text-gray-12 font-bold text-2xl mb-1">{product.name}</h2>
Expand All @@ -87,11 +94,19 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
disabled={!canModifyOrder()}
onSelect={() => setIsEditAddressModalOpen(true)}
className="cursor-pointer hover:bg-gray-100"
>
Change delivery address
</DropdownMenuItem>
<DropdownMenuItem
disabled={!canModifyOrder()}
onSelect={() => setIsCancelOrderModalOpen(true)}
className="cursor-pointer hover:bg-gray-100"
>
Cancel Order
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
Expand Down Expand Up @@ -202,6 +217,12 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({
order={order}
onOrderUpdate={handleOrderUpdate}
/>
<CancelOrderModal
isOpen={isCancelOrderModalOpen}
onOpenChange={setIsCancelOrderModalOpen}
order={order}
onOrderUpdate={handleOrderUpdate}
/>
</div>
);
};
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/gen/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/stripe/refunds/{order_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/** Refund Payment Intent */
put: operations["refund_payment_intent_stripe_refunds__order_id__put"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/stripe/webhook": {
parameters: {
query?: never;
Expand Down Expand Up @@ -1090,6 +1107,13 @@ export interface components {
/** Files */
files: string[];
};
/** CancelReason */
CancelReason: {
/** Reason */
reason: string;
/** Details */
details: string;
};
/** ClientIdResponse */
ClientIdResponse: {
/** Client Id */
Expand All @@ -1107,6 +1131,14 @@ export interface components {
/** Session Id */
session_id: string;
};
/** CreateRefundsRequest */
CreateRefundsRequest: {
/** Payment Intent Id */
payment_intent_id: string;
cancel_reason: components["schemas"]["CancelReason"];
/** Amount */
amount: number;
};
/** DeleteTokenResponse */
DeleteTokenResponse: {
/** Message */
Expand Down Expand Up @@ -1428,6 +1460,8 @@ export interface components {
stripe_checkout_session_id: string;
/** Stripe Payment Intent Id */
stripe_payment_intent_id: string;
/** Stripe Refund Id */
stripe_refund_id?: string | null;
/** Created At */
created_at: number;
/** Updated At */
Expand Down Expand Up @@ -3348,6 +3382,41 @@ export interface operations {
};
};
};
refund_payment_intent_stripe_refunds__order_id__put: {
parameters: {
query?: never;
header?: never;
path: {
order_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["CreateRefundsRequest"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Order"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
stripe_webhook_stripe_webhook_post: {
parameters: {
query?: never;
Expand Down
3 changes: 3 additions & 0 deletions store/app/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ class Order(StoreBaseModel):
user_email: str
stripe_checkout_session_id: str
stripe_payment_intent_id: str
stripe_refund_id: str | None = None
created_at: int
updated_at: int
status: OrderStatus
Expand All @@ -629,6 +630,7 @@ def create(
amount: int,
currency: str,
quantity: int,
stripe_refund_id: str | None = None,
product_id: str | None = None,
status: OrderStatus = "processing",
shipping_name: str | None = None,
Expand All @@ -646,6 +648,7 @@ def create(
user_email=user_email,
stripe_checkout_session_id=stripe_checkout_session_id,
stripe_payment_intent_id=stripe_payment_intent_id,
stripe_refund_id=stripe_refund_id,
created_at=now,
updated_at=now,
status=status,
Expand Down
Loading

0 comments on commit 488dd95

Please sign in to comment.