Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ DB_PASSWORD=your_db_password

# Admin Database (for database creation/deletion)
ADMIN_DB_NAME=postgres

# Culqui Keys
CULQI_PRIVATE_KEY="sk_test_xxx"
VITE_CULQI_PUBLIC_KEY="pk_test_xxx"
8 changes: 7 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
DATABASE_URL="postgresql://diego@localhost:5432/fullstock_test?schema=public"

# Admin Database (for database creation/deletion)
ADMIN_DB_NAME=postgres
ADMIN_DB_NAME=postgres

# Cloud Storage base url
CS_BASE_URL="https://fullstock-images.s3.us-east-2.amazonaws.com"

CULQI_PRIVATE_KEY="sk_test_EC8oOLd3ZiCTKqjN"
VITE_CULQI_PUBLIC_KEY="pk_test_Ws4NXfH95QXlZgaz"
5 changes: 5 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ jobs:
e2e-test:
runs-on: ubuntu-latest
needs: [test]
env:
CS_BASE_URL: "https://fullstock-images.s3.us-east-2.amazonaws.com"
CULQI_PRIVATE_KEY: "sk_test_EC8oOLd3ZiCTKqjN"
VITE_CULQI_PUBLIC_KEY: "pk_test_Ws4NXfH95QXlZgaz"
DATABASE_URL: "postgresql://diego@localhost:5432/fullstock_test?schema=public"
services:
postgres:
image: postgres:15
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"prisma:studio": "prisma studio",
"prisma:seed": "prisma db seed",
"test:prisma:migrate:deploy": "dotenv -e .env.test -- prisma migrate deploy",
"test:prisma:migrate:reset": "dotenv -e .env.test -- prisma migrate reset",
"test:e2e": "playwright test",
"test:prisma:seed": "dotenv -e .env.test prisma db seed"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "orders" ADD COLUMN "payment_id" TEXT;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ model Order {
userId Int @map("user_id")
totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
email String
paymentId String? @map("payment_id")
firstName String @map("first_name")
lastName String @map("last_name")
company String?
Expand Down
Empty file.
43 changes: 41 additions & 2 deletions src/e2e/guest-create-order.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// import { createOrderFormData } from "@/lib/utils.tests";
import { expect, test } from "@playwright/test";

import { baseUrl, cleanDatabase, createOrderFormData } from "./utils-tests-e2e";
import {
baseUrl,
cleanDatabase,
createOrderFormData,
creditCards,
} from "./utils-tests-e2e";

export type OrderFormData = Record<string, string>;

Expand Down Expand Up @@ -33,9 +38,43 @@ test.describe("Guest", () => {

await page.getByRole("button", { name: "Confirmar Orden" }).click();

const checkoutFrame = page.locator('iframe[name="checkout_frame"]');
await expect(checkoutFrame).toBeVisible({ timeout: 10000 });

const validCard = creditCards.valid;

await checkoutFrame
.contentFrame()
.getByRole("textbox", { name: "#### #### #### ####" })
.fill(validCard.number);

await expect(
page.getByText("¡Muchas gracias por tu compra!")
checkoutFrame.contentFrame().getByRole("img", { name: "Culqi icon" })
).toBeVisible();

await checkoutFrame
.contentFrame()
.getByRole("textbox", { name: "MM/AA" })
.fill(validCard.exp);

await checkoutFrame
.contentFrame()
.getByRole("textbox", { name: "CVV" })
.fill(validCard.cvv);

await checkoutFrame
.contentFrame()
.getByRole("textbox", { name: "correo@electronico.com" })
.fill(orderForm["Correo electrónico"]);

await checkoutFrame
.contentFrame()
.getByRole("button", { name: "Pagar S/" })
.click();

await expect(page.getByText("¡Muchas gracias por tu compra!")).toBeVisible({
timeout: 10000,
});
await expect(page.getByTestId("orderId")).toBeVisible();
});
});
38 changes: 36 additions & 2 deletions src/e2e/user-create-order.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { prisma } from "@/db/prisma";
import { hashPassword } from "@/lib/security";
import type { CreateUserDTO } from "@/models/user.model";

import { baseUrl, cleanDatabase } from "./utils-tests-e2e";
import { baseUrl, cleanDatabase, creditCards } from "./utils-tests-e2e";

test.beforeEach(async () => {
await cleanDatabase();
Expand Down Expand Up @@ -76,9 +76,43 @@ test.describe("User", () => {

await page.getByRole("button", { name: "Confirmar Orden" }).click();

const checkoutFrame = page.locator('iframe[name="checkout_frame"]');
await expect(checkoutFrame).toBeVisible({ timeout: 10000 });

const validCard = creditCards.valid;

await checkoutFrame
.contentFrame()
.getByRole("textbox", { name: "#### #### #### ####" })
.fill(validCard.number);

await expect(
page.getByText("¡Muchas gracias por tu compra!")
checkoutFrame.contentFrame().getByRole("img", { name: "Culqi icon" })
).toBeVisible();

await checkoutFrame
.contentFrame()
.getByRole("textbox", { name: "MM/AA" })
.fill(validCard.exp);

await checkoutFrame
.contentFrame()
.getByRole("textbox", { name: "CVV" })
.fill(validCard.cvv);

await checkoutFrame
.contentFrame()
.getByRole("textbox", { name: "correo@electronico.com" })
.fill(loginForm["Correo electrónico"]);

await checkoutFrame
.contentFrame()
.getByRole("button", { name: "Pagar S/" })
.click();

await expect(page.getByText("¡Muchas gracias por tu compra!")).toBeVisible({
timeout: 10000,
});
await expect(page.getByTestId("orderId")).toBeVisible();
});
});
13 changes: 13 additions & 0 deletions src/e2e/utils-tests-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ export const createOrderFormData = (
...overrides,
});

export const creditCards = {
valid: {
number: "4111 1111 1111 1111",
exp: "12/30",
cvv: "123",
},
declined: {
number: "4000 0200 0000 0000",
exp: "12/30",
cvv: "354",
},
};

export async function cleanDatabase() {
await prisma.order.deleteMany();
await prisma.cart.deleteMany();
Expand Down
78 changes: 78 additions & 0 deletions src/hooks/use-culqui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useEffect, useRef, useState } from "react";

export type CulqiChargeError = {
object: "error";
type: string;
charge_id: string;
code: string;
decline_code: string | null;
merchant_message: string;
user_message: string;
};

export interface CulqiInstance {
open: () => void;
close: () => void;
token?: { id: string };
error?: Error;
culqi?: () => void;
}

export type CulqiConstructorType = new (
publicKey: string,
config: object
) => CulqiInstance;

declare global {
interface Window {
CulqiCheckout?: CulqiConstructorType;
}
}

// Return type explicitly includes the constructor function
export function useCulqi() {
const [CulqiCheckout, setCulqiCheckout] =
useState<CulqiConstructorType | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
const scriptRef = useRef<HTMLScriptElement | null>(null);

useEffect(() => {
if (window.CulqiCheckout) {
setCulqiCheckout(() => window.CulqiCheckout!);
return;
}

setLoading(true);
const script = document.createElement("script");
script.src = "https://js.culqi.com/checkout-js";
script.async = true;
scriptRef.current = script;

script.onload = () => {
if (window.CulqiCheckout) {
setCulqiCheckout(() => window.CulqiCheckout!);
} else {
setError(
new Error("Culqi script loaded but CulqiCheckout object not found")
);
}
setLoading(false);
};

script.onerror = () => {
setError(new Error("Failed to load CulqiCheckout script"));
setLoading(false);
};

document.head.appendChild(script);

return () => {
if (scriptRef.current) {
scriptRef.current.remove();
}
};
}, []);

return { CulqiCheckout, loading, error };
}
2 changes: 2 additions & 0 deletions src/lib/utils.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export const createTestOrder = (overrides: Partial<Order> = {}): Order => {
createdAt: new Date(),
updatedAt: new Date(),
...details, // Expande todos los campos de contacto sin undefined
paymentId: `payment-id-${Math.random()}`,
...overrides,
} satisfies Order;
};
Expand All @@ -181,6 +182,7 @@ export const createTestDBOrder = (
phone: "123456789",
createdAt: new Date(),
updatedAt: new Date(),
paymentId: `payment-id-${Math.random()}`,
...overrides,
} satisfies PrismaOrder;
};
10 changes: 5 additions & 5 deletions src/routes/account/orders/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export default function Orders({ loaderData }: Route.ComponentProps) {
Total
</dt>
<dd className="mt-1 font-medium text-foreground">
{order.totalAmount.toLocaleString("en-US", {
{order.totalAmount.toLocaleString("es-PE", {
style: "currency",
currency: "USD",
currency: "PEN",
})}
</dd>
</div>
Expand Down Expand Up @@ -102,19 +102,19 @@ export default function Orders({ loaderData }: Route.ComponentProps) {
{item.title}
</div>
<div className="mt-1 sm:hidden">
{item.quantity} × ${item.price.toFixed(2)}
{item.quantity} × S/{item.price.toFixed(2)}
</div>
</div>
</div>
</td>
<td className="py-6 pr-8 text-center hidden sm:table-cell">
${item.price.toFixed(2)}
S/{item.price.toFixed(2)}
</td>
<td className="py-6 pr-8 text-center hidden sm:table-cell">
{item.quantity}
</td>
<td className="py-6 pr-8 whitespace-nowrap text-center font-medium text-foreground">
${(item.price * item.quantity).toFixed(2)}
S/{(item.price * item.quantity).toFixed(2)}
</td>
</tr>
))}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/cart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function Cart({ loaderData }: Route.ComponentProps) {
))}
<div className="flex justify-between p-6 text-base font-medium border-b">
<p>Total</p>
<p>${total.toFixed(2)}</p>
<p>S/{total.toFixed(2)}</p>
</div>
<div className="p-6">
<Button size="lg" className="w-full" asChild>
Expand Down
2 changes: 1 addition & 1 deletion src/routes/category/components/product-card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function ProductCard({ product }: ProductCardProps) {
<div className="flex grow flex-col gap-2 p-4">
<h2 className="text-sm font-medium">{product.title}</h2>
<p className="text-sm text-muted-foreground">{product.description}</p>
<p className="mt-auto text-base font-medium">${product.price}</p>
<p className="mt-auto text-base font-medium">S/{product.price}</p>
</div>
{product.isOnSale && (
<span className="absolute top-0 right-0 rounded-bl-xl bg-primary px-2 py-1 text-sm font-medium text-primary-foreground">
Expand Down
Loading