feat: products crud

This commit is contained in:
swve 2024-10-16 00:31:36 +02:00
parent 7d81afc396
commit 4c8cb42978
12 changed files with 677 additions and 39 deletions

View file

@ -1,21 +1,22 @@
from datetime import datetime
from enum import Enum
from typing import Literal, Optional
from typing import Optional
from pydantic import BaseModel
from sqlalchemy import JSON
from sqlmodel import Field, SQLModel, Column, BigInteger, ForeignKey
# Stripe provider config
class StripeProviderConfig(BaseModel):
stripe_key: str = ""
stripe_secret_key: str = ""
stripe_webhook_secret: str = ""
# PaymentsConfig
class PaymentProviderEnum(str, Enum):
STRIPE = "stripe"
class PaymentsConfigBase(SQLModel):
enabled: bool = False
enabled: bool = True
provider: PaymentProviderEnum = PaymentProviderEnum.STRIPE
provider_config: dict = Field(default={}, sa_column=Column(JSON))
@ -34,7 +35,7 @@ class PaymentsConfigCreate(PaymentsConfigBase):
class PaymentsConfigUpdate(PaymentsConfigBase):
enabled: Optional[bool] = False
enabled: Optional[bool] = True
provider_config: Optional[dict] = None

View file

@ -0,0 +1,14 @@
from enum import Enum
from sqlmodel import SQLModel, Field, Column, BigInteger, ForeignKey, String, JSON
from typing import Optional
from datetime import datetime
class PaymentCourseBase(SQLModel):
course_id: int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
payment_product_id: int = Field(sa_column=Column(BigInteger, ForeignKey("paymentsproduct.id", ondelete="CASCADE")))
org_id: int = Field(sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE")))
class PaymentCourse(PaymentCourseBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
creation_date: datetime = Field(default=datetime.now())
update_date: datetime = Field(default=datetime.now())

View file

@ -0,0 +1,38 @@
from enum import Enum
from sqlmodel import SQLModel, Field, Column, BigInteger, ForeignKey, String, JSON
from typing import Optional
from datetime import datetime
class PaymentProductTypeEnum(str, Enum):
SUBSCRIPTION = "subscription"
ONE_TIME = "one_time"
class PaymentsProductBase(SQLModel):
name: str = ""
description: Optional[str] = ""
product_type: PaymentProductTypeEnum = PaymentProductTypeEnum.ONE_TIME
benefits: str = ""
amount: float = 0.0
class PaymentsProduct(PaymentsProductBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
)
payments_config_id: int = Field(sa_column=Column(BigInteger, ForeignKey("paymentsconfig.id", ondelete="CASCADE")))
provider_product_id: str = Field(sa_column=Column(String))
creation_date: datetime = Field(default=datetime.now())
update_date: datetime = Field(default=datetime.now())
class PaymentsProductCreate(PaymentsProductBase):
pass
class PaymentsProductUpdate(PaymentsProductBase):
pass
class PaymentsProductRead(PaymentsProductBase):
id: int
org_id: int
payments_config_id: int
creation_date: datetime
update_date: datetime

View file

@ -0,0 +1,19 @@
from enum import Enum
from sqlmodel import SQLModel, Field, Column, BigInteger, ForeignKey, String, JSON
from typing import Optional
from datetime import datetime
class PaymentUserStatusEnum(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
class PaymentsUserBase(SQLModel):
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("user.id", ondelete="CASCADE")))
status: PaymentUserStatusEnum = PaymentUserStatusEnum.ACTIVE
payment_product_id: int = Field(sa_column=Column(BigInteger, ForeignKey("paymentsproduct.id", ondelete="CASCADE")))
org_id: int = Field(sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE")))
class PaymentsUser(PaymentsUserBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
creation_date: datetime = Field(default=datetime.now())
update_date: datetime = Field(default=datetime.now())

View file

@ -10,6 +10,9 @@ from src.services.payments.payments import (
update_payments_config,
delete_payments_config,
)
from src.db.payments.payments_products import PaymentsProduct, PaymentsProductCreate, PaymentsProductRead, PaymentsProductUpdate
from src.services.payments.payments_products import create_payments_product, delete_payments_product, get_payments_product, list_payments_products, update_payments_product
router = APIRouter()
@ -51,3 +54,54 @@ async def api_delete_payments_config(
):
await delete_payments_config(request, org_id, current_user, db_session)
return {"message": "Payments config deleted successfully"}
@router.post("/{org_id}/products")
async def api_create_payments_product(
request: Request,
org_id: int,
payments_product: PaymentsProductCreate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> PaymentsProductRead:
return await create_payments_product(request, org_id, payments_product, current_user, db_session)
@router.get("/{org_id}/products")
async def api_get_payments_products(
request: Request,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> list[PaymentsProductRead]:
return await list_payments_products(request, org_id, current_user, db_session)
@router.get("/{org_id}/products/{product_id}")
async def api_get_payments_product(
request: Request,
org_id: int,
product_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> PaymentsProductRead:
return await get_payments_product(request, org_id, product_id, current_user, db_session)
@router.put("/{org_id}/products/{product_id}")
async def api_update_payments_product(
request: Request,
org_id: int,
product_id: int,
payments_product: PaymentsProductUpdate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> PaymentsProductRead:
return await update_payments_product(request, org_id, product_id, payments_product, current_user, db_session)
@router.delete("/{org_id}/products/{product_id}")
async def api_delete_payments_product(
request: Request,
org_id: int,
product_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
await delete_payments_product(request, org_id, product_id, current_user, db_session)
return {"message": "Payments product deleted successfully"}

View file

@ -0,0 +1,153 @@
from fastapi import HTTPException, Request
from sqlmodel import Session, select
from src.db.payments.payments import PaymentsConfig
from src.db.payments.payments_products import (
PaymentsProduct,
PaymentsProductCreate,
PaymentsProductUpdate,
PaymentsProductRead,
)
from src.db.users import PublicUser, AnonymousUser
from src.db.organizations import Organization
from src.services.orgs.orgs import rbac_check
from datetime import datetime
from uuid import uuid4
async def create_payments_product(
request: Request,
org_id: int,
payments_product: PaymentsProductCreate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> PaymentsProductRead:
# Check if organization exists
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
# Check if payments config exists and has a valid id
statement = select(PaymentsConfig).where(PaymentsConfig.org_id == org_id)
config = db_session.exec(statement).first()
if not config or config.id is None:
raise HTTPException(status_code=404, detail="Valid payments config not found")
# Create new payments product
new_product = PaymentsProduct(**payments_product.model_dump(), org_id=org_id, payments_config_id=config.id)
new_product.creation_date = datetime.now()
new_product.update_date = datetime.now()
db_session.add(new_product)
db_session.commit()
db_session.refresh(new_product)
return PaymentsProductRead.model_validate(new_product)
async def get_payments_product(
request: Request,
org_id: int,
product_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> PaymentsProductRead:
# Check if organization exists
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
# Get payments product
statement = select(PaymentsProduct).where(PaymentsProduct.id == product_id, PaymentsProduct.org_id == org_id)
product = db_session.exec(statement).first()
if not product:
raise HTTPException(status_code=404, detail="Payments product not found")
return PaymentsProductRead.model_validate(product)
async def update_payments_product(
request: Request,
org_id: int,
product_id: int,
payments_product: PaymentsProductUpdate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> PaymentsProductRead:
# Check if organization exists
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Get existing payments product
statement = select(PaymentsProduct).where(PaymentsProduct.id == product_id, PaymentsProduct.org_id == org_id)
product = db_session.exec(statement).first()
if not product:
raise HTTPException(status_code=404, detail="Payments product not found")
# Update product
for key, value in payments_product.model_dump().items():
setattr(product, key, value)
product.update_date = datetime.now()
db_session.add(product)
db_session.commit()
db_session.refresh(product)
return PaymentsProductRead.model_validate(product)
async def delete_payments_product(
request: Request,
org_id: int,
product_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> None:
# Check if organization exists
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "delete", db_session)
# Get existing payments product
statement = select(PaymentsProduct).where(PaymentsProduct.id == product_id, PaymentsProduct.org_id == org_id)
product = db_session.exec(statement).first()
if not product:
raise HTTPException(status_code=404, detail="Payments product not found")
# Delete product
db_session.delete(product)
db_session.commit()
async def list_payments_products(
request: Request,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> list[PaymentsProductRead]:
# Check if organization exists
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
# Get payments products ordered by id
statement = select(PaymentsProduct).where(PaymentsProduct.org_id == org_id).order_by(PaymentsProduct.id.desc())
products = db_session.exec(statement).all()
return [PaymentsProductRead.model_validate(product) for product in products]

View file

@ -8,6 +8,7 @@ import { CreditCard, Settings, Repeat, BookOpen, Users, DollarSign } from 'lucid
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
import PaymentsConfigurationPage from '@components/Dashboard/Payments/PaymentsConfigurationPage'
import PaymentsProductPage from '@components/Dashboard/Payments/PaymentsProductPage'
@ -40,9 +41,9 @@ function PaymentsPage({ params }: { params: PaymentsParams }) {
setH1Label('Subscriptions')
setH2Label('Manage your subscription plans')
}
if (selectedSubPage === 'paid-courses') {
setH1Label('Paid Courses')
setH2Label('Manage your paid courses and pricing')
if (selectedSubPage === 'paid-products') {
setH1Label('Paid Products')
setH2Label('Manage your paid products and pricing')
}
if (selectedSubPage === 'customers') {
setH1Label('Customers')
@ -66,18 +67,18 @@ function PaymentsPage({ params }: { params: PaymentsParams }) {
</div>
<div className="flex space-x-5 font-black text-sm">
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/general')}
icon={<CreditCard size={16} />}
label="General"
isActive={selectedSubPage === 'general'}
onClick={() => setSelectedSubPage('general')}
href={getUriWithOrg(params.orgslug, '/dash/payments/customers')}
icon={<Users size={16} />}
label="Customers"
isActive={selectedSubPage === 'customers'}
onClick={() => setSelectedSubPage('customers')}
/>
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/configuration')}
icon={<Settings size={16} />}
label="Configuration"
isActive={selectedSubPage === 'configuration'}
onClick={() => setSelectedSubPage('configuration')}
href={getUriWithOrg(params.orgslug, '/dash/payments/paid-products')}
icon={<BookOpen size={16} />}
label="One-time Products"
isActive={selectedSubPage === 'paid-products'}
onClick={() => setSelectedSubPage('paid-products')}
/>
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/subscriptions')}
@ -87,19 +88,13 @@ function PaymentsPage({ params }: { params: PaymentsParams }) {
onClick={() => setSelectedSubPage('subscriptions')}
/>
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/paid-courses')}
icon={<BookOpen size={16} />}
label="Paid Courses"
isActive={selectedSubPage === 'paid-courses'}
onClick={() => setSelectedSubPage('paid-courses')}
/>
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/customers')}
icon={<Users size={16} />}
label="Customers"
isActive={selectedSubPage === 'customers'}
onClick={() => setSelectedSubPage('customers')}
href={getUriWithOrg(params.orgslug, '/dash/payments/configuration')}
icon={<Settings size={16} />}
label="Configuration"
isActive={selectedSubPage === 'configuration'}
onClick={() => setSelectedSubPage('configuration')}
/>
</div>
</div>
<div className="h-6"></div>
@ -112,8 +107,8 @@ function PaymentsPage({ params }: { params: PaymentsParams }) {
>
{selectedSubPage === 'general' && <div>General</div>}
{selectedSubPage === 'configuration' && <PaymentsConfigurationPage />}
{selectedSubPage === 'paid-products' && <PaymentsProductPage />}
{selectedSubPage === 'subscriptions' && <div>Subscriptions</div>}
{selectedSubPage === 'paid-courses' && <div>Paid Courses</div>}
{selectedSubPage === 'customers' && <div>Customers</div>}
</motion.div>
</div>
@ -124,9 +119,8 @@ const TabLink = ({ href, icon, label, isActive, onClick }: { href: string, icon:
<Link href={href}>
<div
onClick={onClick}
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
isActive ? 'border-b-4' : 'opacity-50'
} cursor-pointer`}
className={`py-2 w-fit text-center border-black transition-all ease-linear ${isActive ? 'border-b-4' : 'opacity-50'
} cursor-pointer`}
>
<div className="flex items-center space-x-2.5 mx-2">
{icon}

View file

@ -1,17 +1,13 @@
'use client'
import PageLoading from '@components/Objects/Loaders/PageLoading';
import { useSession } from 'next-auth/react';
import React, { useContext, createContext, useEffect } from 'react'
import React, { useContext, createContext } from 'react'
export const SessionContext = createContext({}) as any
function LHSessionProvider({ children }: { children: React.ReactNode }) {
const session = useSession();
useEffect(() => {
}, [])
if (session && session.status == 'loading') {
return <PageLoading />
}

View file

@ -0,0 +1,180 @@
'use client';
import React, { useState } from 'react'
import { useOrg } from '@components/Contexts/OrgContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import useSWR, { mutate } from 'swr';
import { getProducts, deleteProduct, updateProduct } from '@services/payments/products';
import CreateProductForm from '@components/Dashboard/Payments/SubComponents/CreateProductForm';
import { Plus, Trash2, Pencil, DollarSign, Info } from 'lucide-react';
import Modal from '@components/StyledElements/Modal/Modal';
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
import toast from 'react-hot-toast';
import Link from 'next/link';
import { getUriWithOrg } from '@services/config/config';
function PaymentsProductPage() {
const org = useOrg() as any;
const session = useLHSession() as any;
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingProductId, setEditingProductId] = useState<string | null>(null);
const { data: products, error } = useSWR(
() => org && session ? [`/payments/${org.id}/products`, session.data?.tokens?.access_token] : null,
([url, token]) => getProducts(org.id, token)
);
const handleDeleteProduct = async (productId: string) => {
try {
await deleteProduct(org.id, productId, session.data?.tokens?.access_token);
mutate([`/payments/${org.id}/products`, session.data?.tokens?.access_token]);
toast.success('Product deleted successfully');
} catch (error) {
toast.error('Failed to delete product');
}
}
if (error) return <div>Failed to load products</div>;
if (!products) return <div>Loading...</div>;
return (
<div className="h-full w-full bg-[#f8f8f8]">
<div className="pl-10 pr-10 mx-auto">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Products</h1>
<button
onClick={() => setIsCreateModalOpen(true)}
className="mb-4 flex items-center space-x-2 px-2 py-1.5 rounded-md bg-gradient-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg hover:from-gray-300/50 hover:to-gray-100/80 transition duration-300"
>
<Plus size={18} />
<span className="text-sm font-bold">Create New Product</span>
</button>
</div>
<Modal
isDialogOpen={isCreateModalOpen}
onOpenChange={setIsCreateModalOpen}
dialogTitle="Create New Product"
dialogDescription="Add a new product to your organization"
dialogContent={
<CreateProductForm onSuccess={() => setIsCreateModalOpen(false)} />
}
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{products.data.map((product: any) => (
<div key={product.id} className="bg-white p-4 rounded-lg nice-shadow">
{editingProductId === product.id ? (
<EditProductForm
product={product}
onSuccess={() => setEditingProductId(null)}
onCancel={() => setEditingProductId(null)}
/>
) : (
<div className="flex flex-col space-y-2">
<div className="flex justify-between items-start">
<h3 className="font-bold text-lg">{product.name}</h3>
<div className="flex space-x-2">
<button
onClick={() => setEditingProductId(product.id)}
className="text-blue-500 hover:text-blue-700"
>
<Pencil size={16} />
</button>
<ConfirmationModal
confirmationButtonText="Delete Product"
confirmationMessage="Are you sure you want to delete this product?"
dialogTitle={`Delete ${product.name}?`}
dialogTrigger={
<button className="text-red-500 hover:text-red-700">
<Trash2 size={16} />
</button>
}
functionToExecute={() => handleDeleteProduct(product.id)}
status="warning"
/>
</div>
</div>
<p className="text-gray-600">{product.description}</p>
<p className="mt-2 font-semibold">${product.amount.toFixed(2)}</p>
<p className="text-sm text-gray-500">{product.product_type}</p>
{product.benefits && (
<div className="mt-2">
<h4 className="font-semibold text-sm">Benefits:</h4>
<p className="text-sm text-gray-600">{product.benefits}</p>
</div>
)}
</div>
)}
</div>
))}
</div>
{products.data.length === 0 && (
<div className="flex mx-auto space-x-2 font-semibold mt-3 text-gray-600 items-center">
<Info size={20} />
<p>No products available. Create a new product to get started.</p>
</div>
)}
</div>
</div>
)
}
const EditProductForm = ({ product, onSuccess, onCancel }: { product: any, onSuccess: () => void, onCancel: () => void }) => {
const [name, setName] = useState(product.name);
const [description, setDescription] = useState(product.description);
const [amount, setAmount] = useState(product.amount);
const [benefits, setBenefits] = useState(product.benefits || '');
const org = useOrg() as any;
const session = useLHSession() as any;
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
await updateProduct(org.id, product.id, { name, description, amount, benefits }, session.data?.tokens?.access_token);
mutate([`/payments/${org.id}/products`, session.data?.tokens?.access_token]);
onSuccess();
toast.success('Product updated successfully');
} catch (error) {
toast.error('Failed to update product');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full p-2 border rounded"
placeholder="Product Name"
/>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full p-2 border rounded"
placeholder="Product Description"
/>
<input
type="number"
value={amount}
onChange={(e) => setAmount(parseFloat(e.target.value))}
className="w-full p-2 border rounded"
placeholder="Price"
step="0.01"
/>
<textarea
value={benefits}
onChange={(e) => setBenefits(e.target.value)}
className="w-full p-2 border rounded"
placeholder="Product Benefits"
/>
<div className="flex justify-end space-x-2">
<button type="button" onClick={onCancel} className="px-4 py-2 text-gray-600 border rounded">Cancel</button>
<button type="submit" className="px-4 py-2 text-white bg-blue-500 rounded">Save</button>
</div>
</form>
);
};
export default PaymentsProductPage

View file

@ -0,0 +1,139 @@
import React, { useState } from 'react';
import { useOrg } from '@components/Contexts/OrgContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { createProduct } from '@services/payments/products';
import FormLayout, { ButtonBlack, Input, Textarea, FormField, FormLabelAndMessage, Flex } from '@components/StyledElements/Form/Form';
import * as Form from '@radix-ui/react-form';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import { mutate } from 'swr';
import { getAPIUrl } from '@services/config/config';
interface ProductFormValues {
name: string;
description: string;
product_type: string;
benefits: string;
amount: number;
}
const CreateProductForm: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
const org = useOrg() as any;
const session = useLHSession() as any;
const [isSubmitting, setIsSubmitting] = useState(false);
const validate = (values: any) => {
const errors: any = {};
if (!values.name) {
errors.name = 'Required';
}
if (!values.description) {
errors.description = 'Required';
}
if (!values.amount) {
errors.amount = 'Required';
} else {
const numAmount = Number(values.amount);
if (isNaN(numAmount) || numAmount <= 0) {
errors.amount = 'Amount must be greater than 0';
}
}
return errors;
};
const formik = useFormik<ProductFormValues>({
initialValues: {
name: '',
description: '',
product_type: 'one_time',
benefits: '',
amount: 0,
},
validate,
onSubmit: async (values) => {
setIsSubmitting(true);
try {
const res = await createProduct(org.id, values, session.data?.tokens?.access_token);
if (res.success) {
toast.success('Product created successfully');
mutate([`/payments/${org.id}/products`, session.data?.tokens?.access_token]);
formik.resetForm();
onSuccess(); // Call the onSuccess function to close the modal
} else {
toast.error('Failed to create product');
}
} catch (error) {
console.error('Error creating product:', error);
toast.error('An error occurred while creating the product');
} finally {
setIsSubmitting(false);
}
},
});
return (
<div className="p-5">
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label="Product Name" message={formik.errors.name} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.name}
type="text"
required
/>
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label="Description" message={formik.errors.description} />
<Form.Control asChild>
<Textarea
onChange={formik.handleChange}
value={formik.values.description}
required
/>
</Form.Control>
</FormField>
<FormField name="benefits">
<FormLabelAndMessage label="Benefits" />
<Form.Control asChild>
<Textarea
onChange={formik.handleChange}
value={formik.values.benefits}
/>
</Form.Control>
</FormField>
<FormField name="amount">
<FormLabelAndMessage label="Amount" message={formik.errors.amount} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.amount}
type="number"
min="0"
step="0.01"
required
/>
</Form.Control>
</FormField>
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
<Form.Submit asChild>
<ButtonBlack type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create Product'}
</ButtonBlack>
</Form.Submit>
</Flex>
</FormLayout>
</div>
);
};
export default CreateProductForm;

View file

@ -0,0 +1,50 @@
import { getAPIUrl } from '@services/config/config';
import { RequestBodyWithAuthHeader, getResponseMetadata } from '@services/utils/ts/requests';
export async function getProducts(orgId: number, access_token: string) {
const result = await fetch(
`${getAPIUrl()}payments/${orgId}/products`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
);
const res = await getResponseMetadata(result);
return res;
}
export async function createProduct(orgId: number, data: any, access_token: string) {
const result = await fetch(
`${getAPIUrl()}payments/${orgId}/products`,
RequestBodyWithAuthHeader('POST', data, null, access_token)
);
const res = await getResponseMetadata(result);
return res;
}
export async function updateProduct(orgId: number, productId: string, data: any, access_token: string) {
const result = await fetch(
`${getAPIUrl()}payments/${orgId}/products/${productId}`,
RequestBodyWithAuthHeader('PUT', data, null, access_token)
);
const res = await getResponseMetadata(result);
return res;
}
export async function deleteProduct(orgId: number, productId: string, access_token: string) {
const result = await fetch(
`${getAPIUrl()}payments/${orgId}/products/${productId}`,
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
);
const res = await getResponseMetadata(result);
return res;
}
export async function getProductDetails(orgId: number, productId: string, access_token: string) {
const result = await fetch(
`${getAPIUrl()}payments/${orgId}/products/${productId}`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
);
const res = await getResponseMetadata(result);
return res;
}