refund roadmap pr-1 and pr-2

This commit is contained in:
2026-05-11 13:04:20 +07:00
parent d2b0a780d5
commit 54f4569107
36 changed files with 5750 additions and 19 deletions
+5 -1
View File
@@ -6,7 +6,11 @@
"Bash(Get-ChildItem -Path \"c:\\\\development\\\\DIOS\\\\weekly-project\\\\setrip\" -Force)",
"Bash(Select-Object Name, PSIsContainer)",
"Bash(npx tsc *)",
"Bash(echo \"exitcode=$?\")"
"Bash(echo \"exitcode=$?\")",
"PowerShell(npx prisma generate 2>&1)",
"PowerShell(npx tsc --noEmit 2>&1)",
"PowerShell(npx eslint server/services/refund.service.ts server/repositories/refund.repo.ts features/refund app/admin/refunds 2>&1)",
"PowerShell(npx eslint server lib features app 2>&1)"
]
}
}
+2 -2
View File
@@ -16,8 +16,8 @@
## Forbidden
- Jangan query database langsung di component
- Jangan buat arsitektur over-engineered
- Jangan menambahkan dependency tanpa kebutuhan jelas
- Jangan buat arsitektur over-engineered, tidak apa apa jika lebih baik untuk performance dan struktur yang baik
- Jangan menambahkan dependency tanpa kebutuhan jelas, tambahkan jika memang dibutuhkan dan gunakan dependency yang aman
## Output Style
+17
View File
@@ -0,0 +1,17 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Admin · Refund Manual",
description:
"Halaman admin untuk meninjau laporan refund dari peserta dan organizer.",
alternates: { canonical: "/admin/refunds" },
robots: { index: false, follow: false },
};
export default function AdminRefundsLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}
+126
View File
@@ -0,0 +1,126 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { isAdminEmail } from "@/lib/admin";
import { refundRepo } from "@/server/repositories/refund.repo";
import { CreateRefundForm } from "@/features/refund/components/create-refund-form";
import {
RefundReviewCard,
type RefundCardData,
} from "@/features/refund/components/refund-review-card";
type Tab = "PENDING" | "APPROVED" | "REJECTED" | "SUCCEEDED" | "FAILED";
const TABS: { key: Tab; label: string }[] = [
{ key: "PENDING", label: "Pending" },
{ key: "APPROVED", label: "Disetujui" },
{ key: "SUCCEEDED", label: "Selesai" },
{ key: "REJECTED", label: "Ditolak" },
{ key: "FAILED", label: "Gagal" },
];
interface PageProps {
searchParams: Promise<{ tab?: string }>;
}
export default async function AdminRefundsPage({ searchParams }: PageProps) {
const session = await getServerSession(authOptions);
if (!session?.user) redirect("/login?callbackUrl=/admin/refunds");
if (!isAdminEmail(session.user.email)) {
return (
<div className="mx-auto max-w-2xl px-4 py-12 text-center">
<p className="text-sm text-neutral-600">
Halaman ini hanya untuk admin SeTrip.
</p>
</div>
);
}
const params = await searchParams;
const tab: Tab = TABS.some((t) => t.key === params.tab)
? (params.tab as Tab)
: "PENDING";
const rows = await refundRepo.listByStatus(tab);
const items: RefundCardData[] = rows.map((r) => ({
id: r.id,
amount: r.amount,
currency: r.currency,
reason: r.reason,
reportedBy: r.reportedBy,
reportNote: r.reportNote,
initiatedBy: r.initiatedBy,
status: r.status,
adminNote: r.adminNote,
createdAt: r.createdAt,
reviewedAt: r.reviewedAt,
succeededAt: r.succeededAt,
failedAt: r.failedAt,
reviewedBy: r.reviewedBy,
booking: {
id: r.booking.id,
amount: r.booking.amount,
status: r.booking.status,
trip: {
id: r.booking.trip.id,
title: r.booking.trip.title,
date: r.booking.trip.date,
},
user: r.booking.user,
payments: r.booking.payments.map((p) => ({
id: p.id,
provider: p.provider,
method: p.method,
amount: p.amount,
status: p.status,
paidAt: p.paidAt,
})),
},
}));
return (
<div className="mx-auto max-w-4xl px-4 py-8 sm:py-12">
<header className="mb-6">
<h1 className="text-2xl font-bold text-neutral-900 sm:text-3xl">
Review Refund Manual
</h1>
<p className="mt-1 text-sm text-neutral-500">
Tinjau laporan refund dari peserta dan organizer. Setiap refund harus
melalui approval admin sebelum dieksekusi.
</p>
</header>
<CreateRefundForm />
<div className="mb-6 flex flex-wrap gap-2">
{TABS.map((t) => (
<a
key={t.key}
href={`/admin/refunds?tab=${t.key}`}
className={`rounded-full px-4 py-1.5 text-sm font-semibold transition-colors ${
tab === t.key
? "bg-primary-600 text-white"
: "bg-neutral-100 text-neutral-600 hover:bg-neutral-200"
}`}
>
{t.label}
</a>
))}
</div>
{items.length === 0 ? (
<div className="rounded-2xl border border-dashed border-neutral-300 bg-white p-10 text-center">
<p className="text-sm text-neutral-500">
Tidak ada refund pada status ini.
</p>
</div>
) : (
<div className="space-y-4">
{items.map((r) => (
<RefundReviewCard key={r.id} refund={r} />
))}
</div>
)}
</div>
);
}
+12
View File
@@ -75,3 +75,15 @@ export type Booking = Prisma.BookingModel
* (di Phase MIDTRANS nanti). Untuk MANUAL biasanya cukup 1 Payment.
*/
export type Payment = Prisma.PaymentModel
/**
* Model Refund
* Refund = financial event terpisah dari Booking. Satu Booking bisa punya
* banyak Refund (partial, multi-tahap). Setiap row auditable: kapan dibuat,
* siapa melaporkan, siapa approve, kapan SUCCEEDED. Never delete — kalau
* gagal, set status=FAILED + alasan.
*
* Di MVP refund dimasukkan admin secara manual berdasarkan laporan dari
* peserta atau organizer (via WhatsApp/email). Phase berikutnya akan
* menambah self-service flow dari user dan organizer.
*/
export type Refund = Prisma.RefundModel
+12
View File
@@ -99,3 +99,15 @@ export type Booking = Prisma.BookingModel
* (di Phase MIDTRANS nanti). Untuk MANUAL biasanya cukup 1 Payment.
*/
export type Payment = Prisma.PaymentModel
/**
* Model Refund
* Refund = financial event terpisah dari Booking. Satu Booking bisa punya
* banyak Refund (partial, multi-tahap). Setiap row auditable: kapan dibuat,
* siapa melaporkan, siapa approve, kapan SUCCEEDED. Never delete — kalau
* gagal, set status=FAILED + alasan.
*
* Di MVP refund dimasukkan admin secara manual berdasarkan laporan dari
* peserta atau organizer (via WhatsApp/email). Phase berikutnya akan
* menambah self-service flow dari user dan organizer.
*/
export type Refund = Prisma.RefundModel
+136
View File
@@ -389,6 +389,74 @@ export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
}
export type EnumRefundReasonFilter<$PrismaModel = never> = {
equals?: $Enums.RefundReason | Prisma.EnumRefundReasonFieldRefInput<$PrismaModel>
in?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel> | $Enums.RefundReason
}
export type EnumRefundReporterFilter<$PrismaModel = never> = {
equals?: $Enums.RefundReporter | Prisma.EnumRefundReporterFieldRefInput<$PrismaModel>
in?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel> | $Enums.RefundReporter
}
export type EnumRefundInitiatorFilter<$PrismaModel = never> = {
equals?: $Enums.RefundInitiator | Prisma.EnumRefundInitiatorFieldRefInput<$PrismaModel>
in?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel> | $Enums.RefundInitiator
}
export type EnumRefundStatusFilter<$PrismaModel = never> = {
equals?: $Enums.RefundStatus | Prisma.EnumRefundStatusFieldRefInput<$PrismaModel>
in?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel> | $Enums.RefundStatus
}
export type EnumRefundReasonWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RefundReason | Prisma.EnumRefundReasonFieldRefInput<$PrismaModel>
in?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundReasonWithAggregatesFilter<$PrismaModel> | $Enums.RefundReason
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel>
_max?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel>
}
export type EnumRefundReporterWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RefundReporter | Prisma.EnumRefundReporterFieldRefInput<$PrismaModel>
in?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundReporterWithAggregatesFilter<$PrismaModel> | $Enums.RefundReporter
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel>
_max?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel>
}
export type EnumRefundInitiatorWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RefundInitiator | Prisma.EnumRefundInitiatorFieldRefInput<$PrismaModel>
in?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundInitiatorWithAggregatesFilter<$PrismaModel> | $Enums.RefundInitiator
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel>
_max?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel>
}
export type EnumRefundStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RefundStatus | Prisma.EnumRefundStatusFieldRefInput<$PrismaModel>
in?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundStatusWithAggregatesFilter<$PrismaModel> | $Enums.RefundStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -750,4 +818,72 @@ export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedEnumRefundReasonFilter<$PrismaModel = never> = {
equals?: $Enums.RefundReason | Prisma.EnumRefundReasonFieldRefInput<$PrismaModel>
in?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel> | $Enums.RefundReason
}
export type NestedEnumRefundReporterFilter<$PrismaModel = never> = {
equals?: $Enums.RefundReporter | Prisma.EnumRefundReporterFieldRefInput<$PrismaModel>
in?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel> | $Enums.RefundReporter
}
export type NestedEnumRefundInitiatorFilter<$PrismaModel = never> = {
equals?: $Enums.RefundInitiator | Prisma.EnumRefundInitiatorFieldRefInput<$PrismaModel>
in?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel> | $Enums.RefundInitiator
}
export type NestedEnumRefundStatusFilter<$PrismaModel = never> = {
equals?: $Enums.RefundStatus | Prisma.EnumRefundStatusFieldRefInput<$PrismaModel>
in?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel> | $Enums.RefundStatus
}
export type NestedEnumRefundReasonWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RefundReason | Prisma.EnumRefundReasonFieldRefInput<$PrismaModel>
in?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundReasonWithAggregatesFilter<$PrismaModel> | $Enums.RefundReason
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel>
_max?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel>
}
export type NestedEnumRefundReporterWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RefundReporter | Prisma.EnumRefundReporterFieldRefInput<$PrismaModel>
in?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundReporterWithAggregatesFilter<$PrismaModel> | $Enums.RefundReporter
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel>
_max?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel>
}
export type NestedEnumRefundInitiatorWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RefundInitiator | Prisma.EnumRefundInitiatorFieldRefInput<$PrismaModel>
in?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundInitiatorWithAggregatesFilter<$PrismaModel> | $Enums.RefundInitiator
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel>
_max?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel>
}
export type NestedEnumRefundStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RefundStatus | Prisma.EnumRefundStatusFieldRefInput<$PrismaModel>
in?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRefundStatusWithAggregatesFilter<$PrismaModel> | $Enums.RefundStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel>
}
+43
View File
@@ -68,6 +68,7 @@ export const BookingStatus = {
PAID: 'PAID',
CANCELLED: 'CANCELLED',
REFUNDED: 'REFUNDED',
PARTIALLY_REFUNDED: 'PARTIALLY_REFUNDED',
EXPIRED: 'EXPIRED'
} as const
@@ -93,3 +94,45 @@ export const PaymentStatus = {
} as const
export type PaymentStatus = (typeof PaymentStatus)[keyof typeof PaymentStatus]
export const RefundReason = {
USER_CANCELLATION: 'USER_CANCELLATION',
ORGANIZER_CANCELLED: 'ORGANIZER_CANCELLED',
TRIP_ISSUE: 'TRIP_ISSUE',
ADMIN_ADJUSTMENT: 'ADMIN_ADJUSTMENT',
DISPUTE_RESOLVED: 'DISPUTE_RESOLVED',
OTHER: 'OTHER'
} as const
export type RefundReason = (typeof RefundReason)[keyof typeof RefundReason]
export const RefundStatus = {
PENDING: 'PENDING',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
PROCESSING: 'PROCESSING',
SUCCEEDED: 'SUCCEEDED',
FAILED: 'FAILED'
} as const
export type RefundStatus = (typeof RefundStatus)[keyof typeof RefundStatus]
export const RefundInitiator = {
USER: 'USER',
ORGANIZER: 'ORGANIZER',
SYSTEM: 'SYSTEM',
ADMIN: 'ADMIN'
} as const
export type RefundInitiator = (typeof RefundInitiator)[keyof typeof RefundInitiator]
export const RefundReporter = {
PARTICIPANT: 'PARTICIPANT',
ORGANIZER: 'ORGANIZER'
} as const
export type RefundReporter = (typeof RefundReporter)[keyof typeof RefundReporter]
File diff suppressed because one or more lines are too long
@@ -393,7 +393,8 @@ export const ModelName = {
TripImage: 'TripImage',
TripParticipant: 'TripParticipant',
Booking: 'Booking',
Payment: 'Payment'
Payment: 'Payment',
Refund: 'Refund'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -409,7 +410,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "userProfile" | "account" | "organizerVerification" | "trip" | "tripReview" | "tripImage" | "tripParticipant" | "booking" | "payment"
modelProps: "user" | "userProfile" | "account" | "organizerVerification" | "trip" | "tripReview" | "tripImage" | "tripParticipant" | "booking" | "payment" | "refund"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -1153,6 +1154,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
Refund: {
payload: Prisma.$RefundPayload<ExtArgs>
fields: Prisma.RefundFieldRefs
operations: {
findUnique: {
args: Prisma.RefundFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload> | null
}
findUniqueOrThrow: {
args: Prisma.RefundFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
}
findFirst: {
args: Prisma.RefundFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload> | null
}
findFirstOrThrow: {
args: Prisma.RefundFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
}
findMany: {
args: Prisma.RefundFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>[]
}
create: {
args: Prisma.RefundCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
}
createMany: {
args: Prisma.RefundCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.RefundCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>[]
}
delete: {
args: Prisma.RefundDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
}
update: {
args: Prisma.RefundUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
}
deleteMany: {
args: Prisma.RefundDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.RefundUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.RefundUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>[]
}
upsert: {
args: Prisma.RefundUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
}
aggregate: {
args: Prisma.RefundAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateRefund>
}
groupBy: {
args: Prisma.RefundGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.RefundGroupByOutputType>[]
}
count: {
args: Prisma.RefundCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.RefundCountAggregateOutputType> | number
}
}
}
}
} & {
other: {
@@ -1365,6 +1440,31 @@ export const PaymentScalarFieldEnum = {
export type PaymentScalarFieldEnum = (typeof PaymentScalarFieldEnum)[keyof typeof PaymentScalarFieldEnum]
export const RefundScalarFieldEnum = {
id: 'id',
bookingId: 'bookingId',
paymentId: 'paymentId',
amount: 'amount',
currency: 'currency',
reason: 'reason',
reportedBy: 'reportedBy',
reportNote: 'reportNote',
initiatedBy: 'initiatedBy',
status: 'status',
idempotencyKey: 'idempotencyKey',
adminNote: 'adminNote',
reviewedById: 'reviewedById',
reviewedAt: 'reviewedAt',
succeededAt: 'succeededAt',
failedAt: 'failedAt',
externalRefundId: 'externalRefundId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type RefundScalarFieldEnum = (typeof RefundScalarFieldEnum)[keyof typeof RefundScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -1587,6 +1687,62 @@ export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$Prisma
/**
* Reference to a field of type 'RefundReason'
*/
export type EnumRefundReasonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundReason'>
/**
* Reference to a field of type 'RefundReason[]'
*/
export type ListEnumRefundReasonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundReason[]'>
/**
* Reference to a field of type 'RefundReporter'
*/
export type EnumRefundReporterFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundReporter'>
/**
* Reference to a field of type 'RefundReporter[]'
*/
export type ListEnumRefundReporterFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundReporter[]'>
/**
* Reference to a field of type 'RefundInitiator'
*/
export type EnumRefundInitiatorFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundInitiator'>
/**
* Reference to a field of type 'RefundInitiator[]'
*/
export type ListEnumRefundInitiatorFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundInitiator[]'>
/**
* Reference to a field of type 'RefundStatus'
*/
export type EnumRefundStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundStatus'>
/**
* Reference to a field of type 'RefundStatus[]'
*/
export type ListEnumRefundStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundStatus[]'>
/**
* Reference to a field of type 'Float'
*/
@@ -1720,6 +1876,7 @@ export type GlobalOmitConfig = {
tripParticipant?: Prisma.TripParticipantOmit
booking?: Prisma.BookingOmit
payment?: Prisma.PaymentOmit
refund?: Prisma.RefundOmit
}
/* Types for Logging */
@@ -60,7 +60,8 @@ export const ModelName = {
TripImage: 'TripImage',
TripParticipant: 'TripParticipant',
Booking: 'Booking',
Payment: 'Payment'
Payment: 'Payment',
Refund: 'Refund'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -252,6 +253,31 @@ export const PaymentScalarFieldEnum = {
export type PaymentScalarFieldEnum = (typeof PaymentScalarFieldEnum)[keyof typeof PaymentScalarFieldEnum]
export const RefundScalarFieldEnum = {
id: 'id',
bookingId: 'bookingId',
paymentId: 'paymentId',
amount: 'amount',
currency: 'currency',
reason: 'reason',
reportedBy: 'reportedBy',
reportNote: 'reportNote',
initiatedBy: 'initiatedBy',
status: 'status',
idempotencyKey: 'idempotencyKey',
adminNote: 'adminNote',
reviewedById: 'reviewedById',
reviewedAt: 'reviewedAt',
succeededAt: 'succeededAt',
failedAt: 'failedAt',
externalRefundId: 'externalRefundId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type RefundScalarFieldEnum = (typeof RefundScalarFieldEnum)[keyof typeof RefundScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
+1
View File
@@ -18,4 +18,5 @@ export type * from './models/TripImage'
export type * from './models/TripParticipant'
export type * from './models/Booking'
export type * from './models/Payment'
export type * from './models/Refund'
export type * from './commonInputTypes'
+142
View File
@@ -257,6 +257,7 @@ export type BookingWhereInput = {
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
participant?: Prisma.XOR<Prisma.TripParticipantScalarRelationFilter, Prisma.TripParticipantWhereInput>
payments?: Prisma.PaymentListRelationFilter
refunds?: Prisma.RefundListRelationFilter
}
export type BookingOrderByWithRelationInput = {
@@ -273,6 +274,7 @@ export type BookingOrderByWithRelationInput = {
user?: Prisma.UserOrderByWithRelationInput
participant?: Prisma.TripParticipantOrderByWithRelationInput
payments?: Prisma.PaymentOrderByRelationAggregateInput
refunds?: Prisma.RefundOrderByRelationAggregateInput
}
export type BookingWhereUniqueInput = Prisma.AtLeast<{
@@ -293,6 +295,7 @@ export type BookingWhereUniqueInput = Prisma.AtLeast<{
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
participant?: Prisma.XOR<Prisma.TripParticipantScalarRelationFilter, Prisma.TripParticipantWhereInput>
payments?: Prisma.PaymentListRelationFilter
refunds?: Prisma.RefundListRelationFilter
}, "id" | "participantId" | "tripId_userId">
export type BookingOrderByWithAggregationInput = {
@@ -338,6 +341,7 @@ export type BookingCreateInput = {
user: Prisma.UserCreateNestedOneWithoutBookingsInput
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
}
export type BookingUncheckedCreateInput = {
@@ -351,6 +355,7 @@ export type BookingUncheckedCreateInput = {
createdAt?: Date | string
updatedAt?: Date | string
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
}
export type BookingUpdateInput = {
@@ -364,6 +369,7 @@ export type BookingUpdateInput = {
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
}
export type BookingUncheckedUpdateInput = {
@@ -377,6 +383,7 @@ export type BookingUncheckedUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
}
export type BookingCreateManyInput = {
@@ -615,6 +622,20 @@ export type BookingUpdateOneRequiredWithoutPaymentsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.BookingUpdateToOneWithWhereWithoutPaymentsInput, Prisma.BookingUpdateWithoutPaymentsInput>, Prisma.BookingUncheckedUpdateWithoutPaymentsInput>
}
export type BookingCreateNestedOneWithoutRefundsInput = {
create?: Prisma.XOR<Prisma.BookingCreateWithoutRefundsInput, Prisma.BookingUncheckedCreateWithoutRefundsInput>
connectOrCreate?: Prisma.BookingCreateOrConnectWithoutRefundsInput
connect?: Prisma.BookingWhereUniqueInput
}
export type BookingUpdateOneRequiredWithoutRefundsNestedInput = {
create?: Prisma.XOR<Prisma.BookingCreateWithoutRefundsInput, Prisma.BookingUncheckedCreateWithoutRefundsInput>
connectOrCreate?: Prisma.BookingCreateOrConnectWithoutRefundsInput
upsert?: Prisma.BookingUpsertWithoutRefundsInput
connect?: Prisma.BookingWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.BookingUpdateToOneWithWhereWithoutRefundsInput, Prisma.BookingUpdateWithoutRefundsInput>, Prisma.BookingUncheckedUpdateWithoutRefundsInput>
}
export type BookingCreateWithoutUserInput = {
id?: string
amount: number
@@ -625,6 +646,7 @@ export type BookingCreateWithoutUserInput = {
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
}
export type BookingUncheckedCreateWithoutUserInput = {
@@ -637,6 +659,7 @@ export type BookingUncheckedCreateWithoutUserInput = {
createdAt?: Date | string
updatedAt?: Date | string
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
}
export type BookingCreateOrConnectWithoutUserInput = {
@@ -690,6 +713,7 @@ export type BookingCreateWithoutTripInput = {
user: Prisma.UserCreateNestedOneWithoutBookingsInput
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
}
export type BookingUncheckedCreateWithoutTripInput = {
@@ -702,6 +726,7 @@ export type BookingUncheckedCreateWithoutTripInput = {
createdAt?: Date | string
updatedAt?: Date | string
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
}
export type BookingCreateOrConnectWithoutTripInput = {
@@ -740,6 +765,7 @@ export type BookingCreateWithoutParticipantInput = {
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
user: Prisma.UserCreateNestedOneWithoutBookingsInput
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
}
export type BookingUncheckedCreateWithoutParticipantInput = {
@@ -752,6 +778,7 @@ export type BookingUncheckedCreateWithoutParticipantInput = {
createdAt?: Date | string
updatedAt?: Date | string
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
}
export type BookingCreateOrConnectWithoutParticipantInput = {
@@ -780,6 +807,7 @@ export type BookingUpdateWithoutParticipantInput = {
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
}
export type BookingUncheckedUpdateWithoutParticipantInput = {
@@ -792,6 +820,7 @@ export type BookingUncheckedUpdateWithoutParticipantInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
}
export type BookingCreateWithoutPaymentsInput = {
@@ -804,6 +833,7 @@ export type BookingCreateWithoutPaymentsInput = {
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
user: Prisma.UserCreateNestedOneWithoutBookingsInput
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
}
export type BookingUncheckedCreateWithoutPaymentsInput = {
@@ -816,6 +846,7 @@ export type BookingUncheckedCreateWithoutPaymentsInput = {
status?: $Enums.BookingStatus
createdAt?: Date | string
updatedAt?: Date | string
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
}
export type BookingCreateOrConnectWithoutPaymentsInput = {
@@ -844,6 +875,7 @@ export type BookingUpdateWithoutPaymentsInput = {
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
}
export type BookingUncheckedUpdateWithoutPaymentsInput = {
@@ -856,6 +888,75 @@ export type BookingUncheckedUpdateWithoutPaymentsInput = {
status?: Prisma.EnumBookingStatusFieldUpdateOperationsInput | $Enums.BookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
}
export type BookingCreateWithoutRefundsInput = {
id?: string
amount: number
currency?: string
status?: $Enums.BookingStatus
createdAt?: Date | string
updatedAt?: Date | string
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
user: Prisma.UserCreateNestedOneWithoutBookingsInput
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
}
export type BookingUncheckedCreateWithoutRefundsInput = {
id?: string
tripId: string
userId: string
participantId: string
amount: number
currency?: string
status?: $Enums.BookingStatus
createdAt?: Date | string
updatedAt?: Date | string
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
}
export type BookingCreateOrConnectWithoutRefundsInput = {
where: Prisma.BookingWhereUniqueInput
create: Prisma.XOR<Prisma.BookingCreateWithoutRefundsInput, Prisma.BookingUncheckedCreateWithoutRefundsInput>
}
export type BookingUpsertWithoutRefundsInput = {
update: Prisma.XOR<Prisma.BookingUpdateWithoutRefundsInput, Prisma.BookingUncheckedUpdateWithoutRefundsInput>
create: Prisma.XOR<Prisma.BookingCreateWithoutRefundsInput, Prisma.BookingUncheckedCreateWithoutRefundsInput>
where?: Prisma.BookingWhereInput
}
export type BookingUpdateToOneWithWhereWithoutRefundsInput = {
where?: Prisma.BookingWhereInput
data: Prisma.XOR<Prisma.BookingUpdateWithoutRefundsInput, Prisma.BookingUncheckedUpdateWithoutRefundsInput>
}
export type BookingUpdateWithoutRefundsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
amount?: Prisma.IntFieldUpdateOperationsInput | number
currency?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumBookingStatusFieldUpdateOperationsInput | $Enums.BookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
}
export type BookingUncheckedUpdateWithoutRefundsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
tripId?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
participantId?: Prisma.StringFieldUpdateOperationsInput | string
amount?: Prisma.IntFieldUpdateOperationsInput | number
currency?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumBookingStatusFieldUpdateOperationsInput | $Enums.BookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
}
export type BookingCreateManyUserInput = {
@@ -879,6 +980,7 @@ export type BookingUpdateWithoutUserInput = {
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
}
export type BookingUncheckedUpdateWithoutUserInput = {
@@ -891,6 +993,7 @@ export type BookingUncheckedUpdateWithoutUserInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
}
export type BookingUncheckedUpdateManyWithoutUserInput = {
@@ -925,6 +1028,7 @@ export type BookingUpdateWithoutTripInput = {
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
}
export type BookingUncheckedUpdateWithoutTripInput = {
@@ -937,6 +1041,7 @@ export type BookingUncheckedUpdateWithoutTripInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
}
export type BookingUncheckedUpdateManyWithoutTripInput = {
@@ -957,10 +1062,12 @@ export type BookingUncheckedUpdateManyWithoutTripInput = {
export type BookingCountOutputType = {
payments: number
refunds: number
}
export type BookingCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
payments?: boolean | BookingCountOutputTypeCountPaymentsArgs
refunds?: boolean | BookingCountOutputTypeCountRefundsArgs
}
/**
@@ -980,6 +1087,13 @@ export type BookingCountOutputTypeCountPaymentsArgs<ExtArgs extends runtime.Type
where?: Prisma.PaymentWhereInput
}
/**
* BookingCountOutputType without action
*/
export type BookingCountOutputTypeCountRefundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.RefundWhereInput
}
export type BookingSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -995,6 +1109,7 @@ export type BookingSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
participant?: boolean | Prisma.TripParticipantDefaultArgs<ExtArgs>
payments?: boolean | Prisma.Booking$paymentsArgs<ExtArgs>
refunds?: boolean | Prisma.Booking$refundsArgs<ExtArgs>
_count?: boolean | Prisma.BookingCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["booking"]>
@@ -1046,6 +1161,7 @@ export type BookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
participant?: boolean | Prisma.TripParticipantDefaultArgs<ExtArgs>
payments?: boolean | Prisma.Booking$paymentsArgs<ExtArgs>
refunds?: boolean | Prisma.Booking$refundsArgs<ExtArgs>
_count?: boolean | Prisma.BookingCountOutputTypeDefaultArgs<ExtArgs>
}
export type BookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
@@ -1066,6 +1182,7 @@ export type $BookingPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
user: Prisma.$UserPayload<ExtArgs>
participant: Prisma.$TripParticipantPayload<ExtArgs>
payments: Prisma.$PaymentPayload<ExtArgs>[]
refunds: Prisma.$RefundPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -1475,6 +1592,7 @@ export interface Prisma__BookingClient<T, Null = never, ExtArgs extends runtime.
user<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
participant<T extends Prisma.TripParticipantDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.TripParticipantDefaultArgs<ExtArgs>>): Prisma.Prisma__TripParticipantClient<runtime.Types.Result.GetResult<Prisma.$TripParticipantPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
payments<T extends Prisma.Booking$paymentsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Booking$paymentsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$PaymentPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
refunds<T extends Prisma.Booking$refundsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Booking$refundsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RefundPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1937,6 +2055,30 @@ export type Booking$paymentsArgs<ExtArgs extends runtime.Types.Extensions.Intern
distinct?: Prisma.PaymentScalarFieldEnum | Prisma.PaymentScalarFieldEnum[]
}
/**
* Booking.refunds
*/
export type Booking$refundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Refund
*/
select?: Prisma.RefundSelect<ExtArgs> | null
/**
* Omit specific fields from the Refund
*/
omit?: Prisma.RefundOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RefundInclude<ExtArgs> | null
where?: Prisma.RefundWhereInput
orderBy?: Prisma.RefundOrderByWithRelationInput | Prisma.RefundOrderByWithRelationInput[]
cursor?: Prisma.RefundWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.RefundScalarFieldEnum | Prisma.RefundScalarFieldEnum[]
}
/**
* Booking without action
*/
+183
View File
@@ -302,6 +302,7 @@ export type PaymentWhereInput = {
createdAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
booking?: Prisma.XOR<Prisma.BookingScalarRelationFilter, Prisma.BookingWhereInput>
refunds?: Prisma.RefundListRelationFilter
}
export type PaymentOrderByWithRelationInput = {
@@ -322,6 +323,7 @@ export type PaymentOrderByWithRelationInput = {
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
booking?: Prisma.BookingOrderByWithRelationInput
refunds?: Prisma.RefundOrderByRelationAggregateInput
}
export type PaymentWhereUniqueInput = Prisma.AtLeast<{
@@ -345,6 +347,7 @@ export type PaymentWhereUniqueInput = Prisma.AtLeast<{
createdAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
booking?: Prisma.XOR<Prisma.BookingScalarRelationFilter, Prisma.BookingWhereInput>
refunds?: Prisma.RefundListRelationFilter
}, "id" | "externalOrderId">
export type PaymentOrderByWithAggregationInput = {
@@ -410,6 +413,7 @@ export type PaymentCreateInput = {
createdAt?: Date | string
updatedAt?: Date | string
booking: Prisma.BookingCreateNestedOneWithoutPaymentsInput
refunds?: Prisma.RefundCreateNestedManyWithoutPaymentInput
}
export type PaymentUncheckedCreateInput = {
@@ -429,6 +433,7 @@ export type PaymentUncheckedCreateInput = {
rejectionReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutPaymentInput
}
export type PaymentUpdateInput = {
@@ -448,6 +453,7 @@ export type PaymentUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
booking?: Prisma.BookingUpdateOneRequiredWithoutPaymentsNestedInput
refunds?: Prisma.RefundUpdateManyWithoutPaymentNestedInput
}
export type PaymentUncheckedUpdateInput = {
@@ -467,6 +473,7 @@ export type PaymentUncheckedUpdateInput = {
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refunds?: Prisma.RefundUncheckedUpdateManyWithoutPaymentNestedInput
}
export type PaymentCreateManyInput = {
@@ -598,6 +605,11 @@ export type PaymentSumOrderByAggregateInput = {
amount?: Prisma.SortOrder
}
export type PaymentNullableScalarRelationFilter = {
is?: Prisma.PaymentWhereInput | null
isNot?: Prisma.PaymentWhereInput | null
}
export type PaymentCreateNestedManyWithoutBookingInput = {
create?: Prisma.XOR<Prisma.PaymentCreateWithoutBookingInput, Prisma.PaymentUncheckedCreateWithoutBookingInput> | Prisma.PaymentCreateWithoutBookingInput[] | Prisma.PaymentUncheckedCreateWithoutBookingInput[]
connectOrCreate?: Prisma.PaymentCreateOrConnectWithoutBookingInput | Prisma.PaymentCreateOrConnectWithoutBookingInput[]
@@ -648,6 +660,22 @@ export type EnumPaymentStatusFieldUpdateOperationsInput = {
set?: $Enums.PaymentStatus
}
export type PaymentCreateNestedOneWithoutRefundsInput = {
create?: Prisma.XOR<Prisma.PaymentCreateWithoutRefundsInput, Prisma.PaymentUncheckedCreateWithoutRefundsInput>
connectOrCreate?: Prisma.PaymentCreateOrConnectWithoutRefundsInput
connect?: Prisma.PaymentWhereUniqueInput
}
export type PaymentUpdateOneWithoutRefundsNestedInput = {
create?: Prisma.XOR<Prisma.PaymentCreateWithoutRefundsInput, Prisma.PaymentUncheckedCreateWithoutRefundsInput>
connectOrCreate?: Prisma.PaymentCreateOrConnectWithoutRefundsInput
upsert?: Prisma.PaymentUpsertWithoutRefundsInput
disconnect?: Prisma.PaymentWhereInput | boolean
delete?: Prisma.PaymentWhereInput | boolean
connect?: Prisma.PaymentWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.PaymentUpdateToOneWithWhereWithoutRefundsInput, Prisma.PaymentUpdateWithoutRefundsInput>, Prisma.PaymentUncheckedUpdateWithoutRefundsInput>
}
export type PaymentCreateWithoutBookingInput = {
id?: string
provider: $Enums.PaymentProvider
@@ -664,6 +692,7 @@ export type PaymentCreateWithoutBookingInput = {
rejectionReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
refunds?: Prisma.RefundCreateNestedManyWithoutPaymentInput
}
export type PaymentUncheckedCreateWithoutBookingInput = {
@@ -682,6 +711,7 @@ export type PaymentUncheckedCreateWithoutBookingInput = {
rejectionReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutPaymentInput
}
export type PaymentCreateOrConnectWithoutBookingInput = {
@@ -732,6 +762,98 @@ export type PaymentScalarWhereInput = {
updatedAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
}
export type PaymentCreateWithoutRefundsInput = {
id?: string
provider: $Enums.PaymentProvider
externalOrderId: string
externalTxId?: string | null
method?: string | null
amount: number
status?: $Enums.PaymentStatus
rawCallback?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
snapToken?: string | null
expiresAt?: Date | string | null
paidAt?: Date | string | null
failedAt?: Date | string | null
rejectionReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
booking: Prisma.BookingCreateNestedOneWithoutPaymentsInput
}
export type PaymentUncheckedCreateWithoutRefundsInput = {
id?: string
bookingId: string
provider: $Enums.PaymentProvider
externalOrderId: string
externalTxId?: string | null
method?: string | null
amount: number
status?: $Enums.PaymentStatus
rawCallback?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
snapToken?: string | null
expiresAt?: Date | string | null
paidAt?: Date | string | null
failedAt?: Date | string | null
rejectionReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
export type PaymentCreateOrConnectWithoutRefundsInput = {
where: Prisma.PaymentWhereUniqueInput
create: Prisma.XOR<Prisma.PaymentCreateWithoutRefundsInput, Prisma.PaymentUncheckedCreateWithoutRefundsInput>
}
export type PaymentUpsertWithoutRefundsInput = {
update: Prisma.XOR<Prisma.PaymentUpdateWithoutRefundsInput, Prisma.PaymentUncheckedUpdateWithoutRefundsInput>
create: Prisma.XOR<Prisma.PaymentCreateWithoutRefundsInput, Prisma.PaymentUncheckedCreateWithoutRefundsInput>
where?: Prisma.PaymentWhereInput
}
export type PaymentUpdateToOneWithWhereWithoutRefundsInput = {
where?: Prisma.PaymentWhereInput
data: Prisma.XOR<Prisma.PaymentUpdateWithoutRefundsInput, Prisma.PaymentUncheckedUpdateWithoutRefundsInput>
}
export type PaymentUpdateWithoutRefundsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
provider?: Prisma.EnumPaymentProviderFieldUpdateOperationsInput | $Enums.PaymentProvider
externalOrderId?: Prisma.StringFieldUpdateOperationsInput | string
externalTxId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
method?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
amount?: Prisma.IntFieldUpdateOperationsInput | number
status?: Prisma.EnumPaymentStatusFieldUpdateOperationsInput | $Enums.PaymentStatus
rawCallback?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
snapToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
expiresAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
paidAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
failedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
booking?: Prisma.BookingUpdateOneRequiredWithoutPaymentsNestedInput
}
export type PaymentUncheckedUpdateWithoutRefundsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
bookingId?: Prisma.StringFieldUpdateOperationsInput | string
provider?: Prisma.EnumPaymentProviderFieldUpdateOperationsInput | $Enums.PaymentProvider
externalOrderId?: Prisma.StringFieldUpdateOperationsInput | string
externalTxId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
method?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
amount?: Prisma.IntFieldUpdateOperationsInput | number
status?: Prisma.EnumPaymentStatusFieldUpdateOperationsInput | $Enums.PaymentStatus
rawCallback?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
snapToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
expiresAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
paidAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
failedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type PaymentCreateManyBookingInput = {
id?: string
provider: $Enums.PaymentProvider
@@ -766,6 +888,7 @@ export type PaymentUpdateWithoutBookingInput = {
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refunds?: Prisma.RefundUpdateManyWithoutPaymentNestedInput
}
export type PaymentUncheckedUpdateWithoutBookingInput = {
@@ -784,6 +907,7 @@ export type PaymentUncheckedUpdateWithoutBookingInput = {
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refunds?: Prisma.RefundUncheckedUpdateManyWithoutPaymentNestedInput
}
export type PaymentUncheckedUpdateManyWithoutBookingInput = {
@@ -805,6 +929,35 @@ export type PaymentUncheckedUpdateManyWithoutBookingInput = {
}
/**
* Count Type PaymentCountOutputType
*/
export type PaymentCountOutputType = {
refunds: number
}
export type PaymentCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
refunds?: boolean | PaymentCountOutputTypeCountRefundsArgs
}
/**
* PaymentCountOutputType without action
*/
export type PaymentCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the PaymentCountOutputType
*/
select?: Prisma.PaymentCountOutputTypeSelect<ExtArgs> | null
}
/**
* PaymentCountOutputType without action
*/
export type PaymentCountOutputTypeCountRefundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.RefundWhereInput
}
export type PaymentSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -824,6 +977,8 @@ export type PaymentSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
createdAt?: boolean
updatedAt?: boolean
booking?: boolean | Prisma.BookingDefaultArgs<ExtArgs>
refunds?: boolean | Prisma.Payment$refundsArgs<ExtArgs>
_count?: boolean | Prisma.PaymentCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["payment"]>
export type PaymentSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -888,6 +1043,8 @@ export type PaymentSelectScalar = {
export type PaymentOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingId" | "provider" | "externalOrderId" | "externalTxId" | "method" | "amount" | "status" | "rawCallback" | "snapToken" | "expiresAt" | "paidAt" | "failedAt" | "rejectionReason" | "createdAt" | "updatedAt", ExtArgs["result"]["payment"]>
export type PaymentInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
booking?: boolean | Prisma.BookingDefaultArgs<ExtArgs>
refunds?: boolean | Prisma.Payment$refundsArgs<ExtArgs>
_count?: boolean | Prisma.PaymentCountOutputTypeDefaultArgs<ExtArgs>
}
export type PaymentIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
booking?: boolean | Prisma.BookingDefaultArgs<ExtArgs>
@@ -900,6 +1057,7 @@ export type $PaymentPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
name: "Payment"
objects: {
booking: Prisma.$BookingPayload<ExtArgs>
refunds: Prisma.$RefundPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -1332,6 +1490,7 @@ readonly fields: PaymentFieldRefs;
export interface Prisma__PaymentClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
booking<T extends Prisma.BookingDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.BookingDefaultArgs<ExtArgs>>): Prisma.Prisma__BookingClient<runtime.Types.Result.GetResult<Prisma.$BookingPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
refunds<T extends Prisma.Payment$refundsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Payment$refundsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RefundPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1777,6 +1936,30 @@ export type PaymentDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
limit?: number
}
/**
* Payment.refunds
*/
export type Payment$refundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Refund
*/
select?: Prisma.RefundSelect<ExtArgs> | null
/**
* Omit specific fields from the Refund
*/
omit?: Prisma.RefundOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RefundInclude<ExtArgs> | null
where?: Prisma.RefundWhereInput
orderBy?: Prisma.RefundOrderByWithRelationInput | Prisma.RefundOrderByWithRelationInput[]
cursor?: Prisma.RefundWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.RefundScalarFieldEnum | Prisma.RefundScalarFieldEnum[]
}
/**
* Payment without action
*/
File diff suppressed because it is too large Load Diff
+192
View File
@@ -229,6 +229,7 @@ export type UserWhereInput = {
bookings?: Prisma.BookingListRelationFilter
organizerVerification?: Prisma.XOR<Prisma.OrganizerVerificationNullableScalarRelationFilter, Prisma.OrganizerVerificationWhereInput> | null
reviewedVerifications?: Prisma.OrganizerVerificationListRelationFilter
reviewedRefunds?: Prisma.RefundListRelationFilter
profile?: Prisma.XOR<Prisma.UserProfileNullableScalarRelationFilter, Prisma.UserProfileWhereInput> | null
}
@@ -250,6 +251,7 @@ export type UserOrderByWithRelationInput = {
bookings?: Prisma.BookingOrderByRelationAggregateInput
organizerVerification?: Prisma.OrganizerVerificationOrderByWithRelationInput
reviewedVerifications?: Prisma.OrganizerVerificationOrderByRelationAggregateInput
reviewedRefunds?: Prisma.RefundOrderByRelationAggregateInput
profile?: Prisma.UserProfileOrderByWithRelationInput
}
@@ -274,6 +276,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
bookings?: Prisma.BookingListRelationFilter
organizerVerification?: Prisma.XOR<Prisma.OrganizerVerificationNullableScalarRelationFilter, Prisma.OrganizerVerificationWhereInput> | null
reviewedVerifications?: Prisma.OrganizerVerificationListRelationFilter
reviewedRefunds?: Prisma.RefundListRelationFilter
profile?: Prisma.XOR<Prisma.UserProfileNullableScalarRelationFilter, Prisma.UserProfileWhereInput> | null
}, "id" | "email">
@@ -327,6 +330,7 @@ export type UserCreateInput = {
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
}
@@ -348,6 +352,7 @@ export type UserUncheckedCreateInput = {
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
}
@@ -369,6 +374,7 @@ export type UserUpdateInput = {
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
}
@@ -390,6 +396,7 @@ export type UserUncheckedUpdateInput = {
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
}
@@ -615,6 +622,22 @@ export type UserUpdateOneRequiredWithoutBookingsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutBookingsInput, Prisma.UserUpdateWithoutBookingsInput>, Prisma.UserUncheckedUpdateWithoutBookingsInput>
}
export type UserCreateNestedOneWithoutReviewedRefundsInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutReviewedRefundsInput, Prisma.UserUncheckedCreateWithoutReviewedRefundsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutReviewedRefundsInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneWithoutReviewedRefundsNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutReviewedRefundsInput, Prisma.UserUncheckedCreateWithoutReviewedRefundsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutReviewedRefundsInput
upsert?: Prisma.UserUpsertWithoutReviewedRefundsInput
disconnect?: Prisma.UserWhereInput | boolean
delete?: Prisma.UserWhereInput | boolean
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutReviewedRefundsInput, Prisma.UserUpdateWithoutReviewedRefundsInput>, Prisma.UserUncheckedUpdateWithoutReviewedRefundsInput>
}
export type UserCreateWithoutProfileInput = {
id?: string
name: string
@@ -633,6 +656,7 @@ export type UserCreateWithoutProfileInput = {
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
}
export type UserUncheckedCreateWithoutProfileInput = {
@@ -653,6 +677,7 @@ export type UserUncheckedCreateWithoutProfileInput = {
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
}
export type UserCreateOrConnectWithoutProfileInput = {
@@ -689,6 +714,7 @@ export type UserUpdateWithoutProfileInput = {
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
}
export type UserUncheckedUpdateWithoutProfileInput = {
@@ -709,6 +735,7 @@ export type UserUncheckedUpdateWithoutProfileInput = {
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
}
export type UserCreateWithoutAccountsInput = {
@@ -728,6 +755,7 @@ export type UserCreateWithoutAccountsInput = {
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
}
@@ -748,6 +776,7 @@ export type UserUncheckedCreateWithoutAccountsInput = {
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
}
@@ -784,6 +813,7 @@ export type UserUpdateWithoutAccountsInput = {
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
}
@@ -804,6 +834,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
}
@@ -824,6 +855,7 @@ export type UserCreateWithoutOrganizerVerificationInput = {
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
}
@@ -844,6 +876,7 @@ export type UserUncheckedCreateWithoutOrganizerVerificationInput = {
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
}
@@ -869,6 +902,7 @@ export type UserCreateWithoutReviewedVerificationsInput = {
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
}
@@ -889,6 +923,7 @@ export type UserUncheckedCreateWithoutReviewedVerificationsInput = {
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
}
@@ -925,6 +960,7 @@ export type UserUpdateWithoutOrganizerVerificationInput = {
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
}
@@ -945,6 +981,7 @@ export type UserUncheckedUpdateWithoutOrganizerVerificationInput = {
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
}
@@ -976,6 +1013,7 @@ export type UserUpdateWithoutReviewedVerificationsInput = {
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
}
@@ -996,6 +1034,7 @@ export type UserUncheckedUpdateWithoutReviewedVerificationsInput = {
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
}
@@ -1016,6 +1055,7 @@ export type UserCreateWithoutTripsInput = {
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
}
@@ -1036,6 +1076,7 @@ export type UserUncheckedCreateWithoutTripsInput = {
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
}
@@ -1072,6 +1113,7 @@ export type UserUpdateWithoutTripsInput = {
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
}
@@ -1092,6 +1134,7 @@ export type UserUncheckedUpdateWithoutTripsInput = {
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
}
@@ -1112,6 +1155,7 @@ export type UserCreateWithoutTripReviewsInput = {
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
}
@@ -1132,6 +1176,7 @@ export type UserUncheckedCreateWithoutTripReviewsInput = {
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
}
@@ -1168,6 +1213,7 @@ export type UserUpdateWithoutTripReviewsInput = {
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
}
@@ -1188,6 +1234,7 @@ export type UserUncheckedUpdateWithoutTripReviewsInput = {
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
}
@@ -1208,6 +1255,7 @@ export type UserCreateWithoutParticipationsInput = {
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
}
@@ -1228,6 +1276,7 @@ export type UserUncheckedCreateWithoutParticipationsInput = {
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
}
@@ -1264,6 +1313,7 @@ export type UserUpdateWithoutParticipationsInput = {
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
}
@@ -1284,6 +1334,7 @@ export type UserUncheckedUpdateWithoutParticipationsInput = {
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
}
@@ -1304,6 +1355,7 @@ export type UserCreateWithoutBookingsInput = {
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
}
@@ -1324,6 +1376,7 @@ export type UserUncheckedCreateWithoutBookingsInput = {
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
}
@@ -1360,6 +1413,7 @@ export type UserUpdateWithoutBookingsInput = {
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
}
@@ -1380,6 +1434,107 @@ export type UserUncheckedUpdateWithoutBookingsInput = {
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
}
export type UserCreateWithoutReviewedRefundsInput = {
id?: string
name: string
email: string
password?: string | null
image?: string | null
emailVerified?: Date | string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
}
export type UserUncheckedCreateWithoutReviewedRefundsInput = {
id?: string
name: string
email: string
password?: string | null
image?: string | null
emailVerified?: Date | string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
}
export type UserCreateOrConnectWithoutReviewedRefundsInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutReviewedRefundsInput, Prisma.UserUncheckedCreateWithoutReviewedRefundsInput>
}
export type UserUpsertWithoutReviewedRefundsInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutReviewedRefundsInput, Prisma.UserUncheckedUpdateWithoutReviewedRefundsInput>
create: Prisma.XOR<Prisma.UserCreateWithoutReviewedRefundsInput, Prisma.UserUncheckedCreateWithoutReviewedRefundsInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutReviewedRefundsInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutReviewedRefundsInput, Prisma.UserUncheckedUpdateWithoutReviewedRefundsInput>
}
export type UserUpdateWithoutReviewedRefundsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutReviewedRefundsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
}
@@ -1395,6 +1550,7 @@ export type UserCountOutputType = {
tripReviews: number
bookings: number
reviewedVerifications: number
reviewedRefunds: number
}
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
@@ -1404,6 +1560,7 @@ export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.I
tripReviews?: boolean | UserCountOutputTypeCountTripReviewsArgs
bookings?: boolean | UserCountOutputTypeCountBookingsArgs
reviewedVerifications?: boolean | UserCountOutputTypeCountReviewedVerificationsArgs
reviewedRefunds?: boolean | UserCountOutputTypeCountReviewedRefundsArgs
}
/**
@@ -1458,6 +1615,13 @@ export type UserCountOutputTypeCountReviewedVerificationsArgs<ExtArgs extends ru
where?: Prisma.OrganizerVerificationWhereInput
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountReviewedRefundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.RefundWhereInput
}
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -1477,6 +1641,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
bookings?: boolean | Prisma.User$bookingsArgs<ExtArgs>
organizerVerification?: boolean | Prisma.User$organizerVerificationArgs<ExtArgs>
reviewedVerifications?: boolean | Prisma.User$reviewedVerificationsArgs<ExtArgs>
reviewedRefunds?: boolean | Prisma.User$reviewedRefundsArgs<ExtArgs>
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["user"]>
@@ -1529,6 +1694,7 @@ export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
bookings?: boolean | Prisma.User$bookingsArgs<ExtArgs>
organizerVerification?: boolean | Prisma.User$organizerVerificationArgs<ExtArgs>
reviewedVerifications?: boolean | Prisma.User$reviewedVerificationsArgs<ExtArgs>
reviewedRefunds?: boolean | Prisma.User$reviewedRefundsArgs<ExtArgs>
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}
@@ -1545,6 +1711,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
bookings: Prisma.$BookingPayload<ExtArgs>[]
organizerVerification: Prisma.$OrganizerVerificationPayload<ExtArgs> | null
reviewedVerifications: Prisma.$OrganizerVerificationPayload<ExtArgs>[]
reviewedRefunds: Prisma.$RefundPayload<ExtArgs>[]
profile: Prisma.$UserProfilePayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
@@ -1971,6 +2138,7 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
bookings<T extends Prisma.User$bookingsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$bookingsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$BookingPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
organizerVerification<T extends Prisma.User$organizerVerificationArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$organizerVerificationArgs<ExtArgs>>): Prisma.Prisma__OrganizerVerificationClient<runtime.Types.Result.GetResult<Prisma.$OrganizerVerificationPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
reviewedVerifications<T extends Prisma.User$reviewedVerificationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$reviewedVerificationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OrganizerVerificationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
reviewedRefunds<T extends Prisma.User$reviewedRefundsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$reviewedRefundsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RefundPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
profile<T extends Prisma.User$profileArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$profileArgs<ExtArgs>>): Prisma.Prisma__UserProfileClient<runtime.Types.Result.GetResult<Prisma.$UserProfilePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
@@ -2566,6 +2734,30 @@ export type User$reviewedVerificationsArgs<ExtArgs extends runtime.Types.Extensi
distinct?: Prisma.OrganizerVerificationScalarFieldEnum | Prisma.OrganizerVerificationScalarFieldEnum[]
}
/**
* User.reviewedRefunds
*/
export type User$reviewedRefundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Refund
*/
select?: Prisma.RefundSelect<ExtArgs> | null
/**
* Omit specific fields from the Refund
*/
omit?: Prisma.RefundOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RefundInclude<ExtArgs> | null
where?: Prisma.RefundWhereInput
orderBy?: Prisma.RefundOrderByWithRelationInput | Prisma.RefundOrderByWithRelationInput[]
cursor?: Prisma.RefundWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.RefundScalarFieldEnum | Prisma.RefundScalarFieldEnum[]
}
/**
* User.profile
*/
+55
View File
@@ -6,17 +6,21 @@ import Image from "next/image";
import { authOptions } from "@/lib/auth";
import { tripService } from "@/server/services/trip.service";
import { bookingService } from "@/server/services/booking.service";
import { bookingRepo } from "@/server/repositories/booking.repo";
import { trustService } from "@/server/services/trust.service";
import { formatRupiah } from "@/lib/utils";
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
import { CancelTripButton } from "@/features/trip/components/cancel-trip-button";
import { CancelBookingButton } from "@/features/booking/components/cancel-booking-button";
import { OrganizerJoinRequests } from "@/features/trip/components/organizer-join-requests";
import { OrganizerTrustPanel } from "@/features/trip/components/organizer-trust-panel";
import { TripProgramBlock } from "@/features/trip/components/trip-program-block";
import { OrganizerPaymentQueue } from "@/features/booking/components/organizer-payment-queue";
import { ImageGallery } from "@/features/trip/components/image-gallery";
import { TripReviewSection } from "@/features/review/components/trip-review-section";
import { RefundPolicySection } from "@/features/refund/components/refund-policy-section";
import { categoryMeta } from "@/lib/activity-category";
import { vibeMeta } from "@/lib/vibe";
import { isFreeTrip } from "@/lib/trip-pricing";
@@ -24,6 +28,7 @@ import {
isPastTripLastDayForReview,
isTripDepartureDayPast,
} from "@/lib/trip-dates";
import { previewRefund } from "@/lib/refund-policy";
export async function generateMetadata({
params,
@@ -137,6 +142,31 @@ export default async function TripDetailPage({
? await bookingService.getAwaitingManualForTrip(trip.id)
: [];
// Booking peserta saat ini — dipakai untuk render CancelBookingButton vs
// tombol "Batal Ikut" biasa. Hanya untuk non-organizer yang ikut trip.
const myBooking =
session?.user && !isOrganizer && currentParticipation
? await bookingService.getByTripAndUser(trip.id, session.user.id)
: null;
// Untuk CancelTripButton: jumlah booking PAID/PARTIALLY_REFUNDED (yang akan
// auto-refund). Hanya dihitung saat organizer mengakses trip yang masih
// bisa dibatalkan.
const canOrganizerCancel =
isOrganizer &&
(trip.status === "OPEN" || trip.status === "FULL") &&
!isDeparturePast;
const paidBookingCount = canOrganizerCancel
? await bookingRepo.countSettledForTrip(trip.id)
: 0;
// Preview refund untuk CancelBookingButton (server-side supaya konsisten
// dengan service yang juga pakai policy yang sama).
const refundPreview =
myBooking && myBooking.status === "PAID" && !isDeparturePast
? previewRefund(myBooking.amount, trip.date)
: null;
const catMeta = categoryMeta(trip.category);
const tripUrl = absoluteUrl(`/trips/${trip.id}`);
@@ -480,8 +510,33 @@ export default async function TripDetailPage({
isFull={spotsLeft <= 0}
tripStatus={trip.status}
isDeparturePast={isDeparturePast}
hideCancelButton={!!refundPreview}
/>
{/* Peserta PAID: cancel + request refund (lewat policy default). */}
{refundPreview && (
<CancelBookingButton
tripId={trip.id}
preview={{
days: refundPreview.days,
refundAmount: refundPreview.refundAmount,
bookingAmount: refundPreview.bookingAmount,
tierLabel: refundPreview.tier.label,
}}
/>
)}
{/* Organizer: batalkan trip (auto-refund peserta PAID). */}
{canOrganizerCancel && (
<CancelTripButton
tripId={trip.id}
paidParticipantCount={paidBookingCount}
/>
)}
{/* Kebijakan refund — transparency sebelum user cancel. */}
{!tripIsFree && <RefundPolicySection />}
<TripReviewSection
tripId={trip.id}
reviews={trip.reviews.map((r) => ({
+16 -2
View File
@@ -158,7 +158,14 @@ function FreeTripSection({
bookingStatus,
}: {
tripId: string;
bookingStatus: "PENDING" | "AWAITING_PAY" | "PAID" | "CANCELLED" | "REFUNDED" | "EXPIRED";
bookingStatus:
| "PENDING"
| "AWAITING_PAY"
| "PAID"
| "CANCELLED"
| "REFUNDED"
| "PARTIALLY_REFUNDED"
| "EXPIRED";
}) {
return (
<section className="rounded-2xl border border-emerald-200 bg-emerald-50/60 p-6 text-center shadow-sm sm:p-8">
@@ -208,7 +215,14 @@ async function PaidTripSection({
organizerId: string;
organizerName: string;
price: number;
bookingStatus: "PENDING" | "AWAITING_PAY" | "PAID" | "CANCELLED" | "REFUNDED" | "EXPIRED";
bookingStatus:
| "PENDING"
| "AWAITING_PAY"
| "PAID"
| "CANCELLED"
| "REFUNDED"
| "PARTIALLY_REFUNDED"
| "EXPIRED";
paymentMarkedAt: Date | null;
paymentPaidAt: Date | null;
}) {
+43
View File
@@ -5,6 +5,7 @@ import { authOptions } from "@/lib/auth";
import { tripService } from "@/server/services/trip.service";
import { paymentService } from "@/server/services/payment.service";
import { bookingService } from "@/server/services/booking.service";
import { refundService } from "@/server/services/refund.service";
import { revalidatePath } from "next/cache";
export async function markParticipantPaidAction(tripId: string) {
@@ -94,3 +95,45 @@ export async function confirmParticipantPaymentAction(
return { error: (err as Error).message };
}
}
/**
* Peserta cancel booking PAID dengan refund request. Server menghitung
* nominal refund pakai policy default (lib/refund-policy.ts) client
* cuma kirim bookingId untuk cegah tampering.
*/
export async function cancelBookingWithRefundAction(tripId: string) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return { error: "Kamu harus login terlebih dahulu" };
}
try {
const booking = await bookingService.getByTripAndUser(
tripId,
session.user.id
);
if (!booking) {
return { error: "Kamu tidak terdaftar di trip ini" };
}
const result = await refundService.requestUserCancellation({
bookingId: booking.id,
userId: session.user.id,
});
revalidatePath(`/trips/${tripId}`);
revalidatePath("/trips");
revalidatePath("/");
revalidatePath("/profile");
revalidatePath("/admin/refunds");
return {
success: true as const,
kind: result.kind,
refundAmount: result.refundAmount,
days: result.days,
};
} catch (err) {
return { error: (err as Error).message };
}
}
@@ -0,0 +1,161 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { cancelBookingWithRefundAction } from "@/features/booking/actions";
import { formatRupiah } from "@/lib/utils";
interface CancelBookingButtonProps {
tripId: string;
/** Hasil preview server-side (dihitung di trip detail page). */
preview: {
days: number;
refundAmount: number;
bookingAmount: number;
tierLabel: string;
};
}
type ServerResult =
| { kind: "REFUND_PENDING"; refundAmount: number; days: number }
| { kind: "CANCELLED_NO_REFUND"; days: number };
export function CancelBookingButton({ tripId, preview }: CancelBookingButtonProps) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [result, setResult] = useState<ServerResult | null>(null);
async function handleConfirm() {
setLoading(true);
setError("");
const res = await cancelBookingWithRefundAction(tripId);
setLoading(false);
if ("error" in res) {
setError(res.error ?? "Terjadi kesalahan");
return;
}
setResult({
kind: res.kind,
refundAmount: res.refundAmount,
days: res.days,
} as ServerResult);
router.refresh();
}
if (result?.kind === "REFUND_PENDING") {
return (
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
<p className="font-semibold">Request refund dibuat.</p>
<p className="mt-1 text-xs">
Refund <span className="font-bold">{formatRupiah(result.refundAmount)}</span>{" "}
menunggu review admin. Setelah disetujui dan ditransfer manual, slot
kamu di trip akan otomatis dibebaskan.
</p>
</div>
);
}
if (result?.kind === "CANCELLED_NO_REFUND") {
return (
<div className="rounded-2xl border border-neutral-200 bg-neutral-50 p-4 text-sm text-neutral-700">
<p className="font-semibold">Booking dibatalkan.</p>
<p className="mt-1 text-xs">
Pembatalan di H-{result.days} berada di luar window refund tidak
ada nominal yang dikembalikan.
</p>
</div>
);
}
if (!open) {
return (
<button
type="button"
onClick={() => setOpen(true)}
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50"
>
Cancel & Request Refund
</button>
);
}
const percentage = preview.bookingAmount
? Math.floor((preview.refundAmount * 100) / preview.bookingAmount)
: 0;
const noRefund = preview.refundAmount === 0;
return (
<div className="space-y-3 rounded-2xl border-2 border-red-200 bg-red-50 p-4 text-sm">
<div>
<p className="font-bold text-red-900">Cancel booking?</p>
<p className="mt-1 text-xs text-red-800/80">
Kamu cancel di <span className="font-semibold">H-{preview.days}</span>{" "}
dari tanggal berangkat.
</p>
</div>
<div className="rounded-xl border border-red-200 bg-white p-3">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">
Estimasi refund (sesuai policy)
</p>
<p className="mt-1 text-lg font-bold text-neutral-900">
{formatRupiah(preview.refundAmount)}
{!noRefund && (
<span className="ml-2 text-xs font-medium text-neutral-500">
({percentage}% dari {formatRupiah(preview.bookingAmount)})
</span>
)}
</p>
<p className="mt-1 text-[11px] text-neutral-500">
Tier: {preview.tierLabel}
</p>
{noRefund ? (
<p className="mt-2 text-xs text-red-700">
Di luar window refund uang tidak dikembalikan. Booking akan
di-cancel langsung.
</p>
) : (
<p className="mt-2 text-xs text-neutral-600">
Refund akan masuk antrian review admin. Setelah disetujui & uang
ditransfer, booking otomatis ditandai{" "}
{percentage === 100 ? "REFUNDED" : "PARTIALLY_REFUNDED"}.
</p>
)}
</div>
{error && (
<div className="rounded-lg bg-white px-3 py-2 text-xs font-medium text-red-700">
{error}
</div>
)}
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={handleConfirm}
disabled={loading}
className="rounded-xl bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700 disabled:opacity-50"
>
{loading
? "Memproses…"
: noRefund
? "Konfirmasi Cancel"
: "Konfirmasi & Request Refund"}
</button>
<button
type="button"
onClick={() => {
setOpen(false);
setError("");
}}
disabled={loading}
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-100 disabled:opacity-50"
>
Batal
</button>
</div>
</div>
);
}
+99
View File
@@ -0,0 +1,99 @@
"use server";
import { getServerSession } from "next-auth";
import { revalidatePath } from "next/cache";
import { authOptions } from "@/lib/auth";
import { isAdminEmail } from "@/lib/admin";
import { refundService } from "@/server/services/refund.service";
import { createRefundSchema, refundDecisionSchema } from "./schemas";
async function requireAdmin() {
const session = await getServerSession(authOptions);
if (!session?.user || !isAdminEmail(session.user.email)) {
return null;
}
return session.user;
}
export async function createRefundAction(formData: FormData) {
const admin = await requireAdmin();
if (!admin) return { error: "Tidak memiliki akses admin" };
const parsed = createRefundSchema.safeParse({
bookingId: formData.get("bookingId") as string,
reason: formData.get("reason") as string,
reportedBy: formData.get("reportedBy") as string,
reportNote: formData.get("reportNote") as string,
amount: (formData.get("amount") as string) || undefined,
});
if (!parsed.success) {
return { error: parsed.error.issues[0].message };
}
try {
await refundService.requestRefund({
bookingId: parsed.data.bookingId,
reason: parsed.data.reason,
reportedBy: parsed.data.reportedBy,
reportNote: parsed.data.reportNote,
amount: parsed.data.amount,
initiatedByAdminId: admin.id,
});
revalidatePath("/admin/refunds");
return { success: true };
} catch (err) {
return { error: (err as Error).message };
}
}
export async function decideRefundAction(formData: FormData) {
const admin = await requireAdmin();
if (!admin) return { error: "Tidak memiliki akses admin" };
const parsed = refundDecisionSchema.safeParse({
refundId: formData.get("refundId") as string,
decision: formData.get("decision") as string,
adminNote: (formData.get("adminNote") as string) || undefined,
});
if (!parsed.success) {
return { error: parsed.error.issues[0].message };
}
const { refundId, decision, adminNote } = parsed.data;
const needsNote = decision === "REJECT" || decision === "SUCCEEDED" || decision === "FAILED";
if (needsNote && (!adminNote || !adminNote.trim())) {
return { error: "Catatan/alasan admin wajib diisi untuk tindakan ini" };
}
try {
if (decision === "APPROVE") {
await refundService.approveRefund({
refundId,
adminId: admin.id,
adminNote,
});
} else if (decision === "REJECT") {
await refundService.rejectRefund({
refundId,
adminId: admin.id,
adminNote: adminNote!,
});
} else if (decision === "SUCCEEDED") {
await refundService.markSucceededManual({
refundId,
adminId: admin.id,
adminNote: adminNote!,
});
} else {
await refundService.markFailed({
refundId,
adminId: admin.id,
adminNote: adminNote!,
});
}
revalidatePath("/admin/refunds");
return { success: true };
} catch (err) {
return { error: (err as Error).message };
}
}
@@ -0,0 +1,206 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { createRefundAction } from "@/features/refund/actions";
const REASON_OPTIONS = [
{ value: "USER_CANCELLATION", label: "Peserta cancel sendiri" },
{ value: "ORGANIZER_CANCELLED", label: "Organizer batalkan trip" },
{ value: "TRIP_ISSUE", label: "Masalah saat/setelah trip" },
{ value: "ADMIN_ADJUSTMENT", label: "Penyesuaian admin" },
{ value: "DISPUTE_RESOLVED", label: "Hasil dispute / chargeback" },
{ value: "OTHER", label: "Lain-lain" },
];
const REPORTER_OPTIONS = [
{ value: "PARTICIPANT", label: "Peserta" },
{ value: "ORGANIZER", label: "Organizer" },
];
export function CreateRefundForm() {
const router = useRouter();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError("");
setLoading(true);
const fd = new FormData(e.currentTarget);
const result = await createRefundAction(fd);
setLoading(false);
if (result.error) {
setError(result.error);
return;
}
(e.target as HTMLFormElement).reset();
setOpen(false);
router.refresh();
}
if (!open) {
return (
<div className="mb-6 flex justify-end">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700"
>
+ Catat Laporan Refund
</button>
</div>
);
}
return (
<form
onSubmit={onSubmit}
className="mb-6 space-y-4 rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:p-6"
>
<header className="flex items-start justify-between gap-3 border-b border-neutral-100 pb-3">
<div>
<h2 className="text-base font-bold text-neutral-900">
Catat Laporan Refund Manual
</h2>
<p className="mt-0.5 text-xs text-neutral-500">
Masukkan laporan yang diterima dari peserta atau organizer (via
WhatsApp/email). Refund akan masuk antrian PENDING untuk di-review.
</p>
</div>
<button
type="button"
onClick={() => {
setOpen(false);
setError("");
}}
className="rounded-lg px-2 py-1 text-xs font-medium text-neutral-500 hover:bg-neutral-100"
>
Tutup
</button>
</header>
{error && (
<div className="rounded-lg bg-red-50 px-3 py-2 text-xs text-red-600">
{error}
</div>
)}
<div className="grid gap-4 sm:grid-cols-2">
<Field label="Booking ID" required>
<input
name="bookingId"
required
placeholder="cuid booking yang dilaporkan"
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm font-mono focus:bg-white"
/>
</Field>
<Field label="Pelapor" required>
<select
name="reportedBy"
required
defaultValue=""
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
>
<option value="" disabled>
Pilih pelapor
</option>
{REPORTER_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</Field>
<Field label="Alasan" required>
<select
name="reason"
required
defaultValue=""
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
>
<option value="" disabled>
Pilih alasan
</option>
{REASON_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</Field>
<Field
label="Nominal (IDR)"
hint="Kosongkan untuk full remaining"
>
<input
name="amount"
inputMode="numeric"
pattern="[0-9]*"
placeholder="mis. 500000"
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
/>
</Field>
</div>
<Field label="Isi laporan" required>
<textarea
name="reportNote"
required
rows={3}
placeholder="Salin/ringkas laporan dari peserta/organizer"
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
/>
</Field>
<div className="flex justify-end gap-2 border-t border-neutral-100 pt-3">
<button
type="button"
onClick={() => {
setOpen(false);
setError("");
}}
className="rounded-xl border border-neutral-200 px-4 py-2 text-sm font-medium text-neutral-600 hover:bg-neutral-50"
>
Batal
</button>
<button
type="submit"
disabled={loading}
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
>
{loading ? "Menyimpan…" : "Simpan Laporan"}
</button>
</div>
</form>
);
}
function Field({
label,
hint,
required,
children,
}: {
label: string;
hint?: string;
required?: boolean;
children: React.ReactNode;
}) {
return (
<label className="block">
<div className="mb-1 flex items-baseline justify-between gap-2">
<span className="text-xs font-semibold uppercase tracking-wide text-neutral-600">
{label}
{required && <span className="text-red-500"> *</span>}
</span>
{hint && <span className="text-xs text-neutral-400">{hint}</span>}
</div>
{children}
</label>
);
}
@@ -0,0 +1,45 @@
import { getRefundPolicyTiers } from "@/lib/refund-policy";
/**
* Display kebijakan refund default di trip detail. Sumber tier:
* lib/refund-policy.ts. Compact agar tidak mendominasi page.
*/
export function RefundPolicySection() {
const tiers = getRefundPolicyTiers();
return (
<details className="rounded-xl border border-neutral-200 bg-neutral-50/60 p-3 text-xs sm:text-sm">
<summary className="cursor-pointer select-none font-semibold text-neutral-700">
🛟 Kebijakan refund saat peserta cancel
</summary>
<div className="mt-2 space-y-2 text-neutral-600">
<p className="text-[11px] text-neutral-500 sm:text-xs">
Kebijakan ini berlaku saat <strong>peserta</strong> cancel booking
yang sudah lunas. Kalau <strong>organizer</strong> membatalkan trip,
peserta yang sudah bayar selalu dapat refund 100%.
</p>
<ul className="space-y-1">
{tiers.map((t) => (
<li key={t.minDaysBefore} className="flex items-baseline gap-2">
<span
className={`inline-flex min-w-[3rem] justify-center rounded-full px-2 py-0.5 text-[10px] font-bold ${
t.refundPercentage >= 80
? "bg-primary-100 text-primary-700"
: t.refundPercentage >= 50
? "bg-amber-100 text-amber-700"
: "bg-red-100 text-red-700"
}`}
>
{t.refundPercentage}%
</span>
<span>{t.label}</span>
</li>
))}
</ul>
<p className="text-[11px] text-neutral-500 sm:text-xs">
Refund diproses manual oleh admin SeTrip perlu 13 hari kerja
setelah disetujui untuk uang masuk ke rekening kamu.
</p>
</div>
</details>
);
}
@@ -0,0 +1,378 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { decideRefundAction } from "@/features/refund/actions";
import { formatRupiah } from "@/lib/utils";
type RefundStatus =
| "PENDING"
| "APPROVED"
| "REJECTED"
| "PROCESSING"
| "SUCCEEDED"
| "FAILED";
type Decision = "APPROVE" | "REJECT" | "SUCCEEDED" | "FAILED";
export type RefundCardData = {
id: string;
amount: number;
currency: string;
reason: string;
reportedBy: "PARTICIPANT" | "ORGANIZER";
reportNote: string;
initiatedBy: string;
status: RefundStatus;
adminNote: string | null;
createdAt: Date;
reviewedAt: Date | null;
succeededAt: Date | null;
failedAt: Date | null;
reviewedBy: { id: string; name: string; email: string } | null;
booking: {
id: string;
amount: number;
status: string;
trip: { id: string; title: string; date: Date };
user: { id: string; name: string; email: string };
payments: {
id: string;
provider: string;
method: string | null;
amount: number;
status: string;
paidAt: Date | null;
}[];
};
};
function formatDate(d: Date): string {
return new Date(d).toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
const REASON_LABEL: Record<string, string> = {
USER_CANCELLATION: "Peserta cancel",
ORGANIZER_CANCELLED: "Organizer batalkan",
TRIP_ISSUE: "Masalah trip",
ADMIN_ADJUSTMENT: "Penyesuaian admin",
DISPUTE_RESOLVED: "Dispute resolved",
OTHER: "Lain-lain",
};
export function RefundReviewCard({ refund }: { refund: RefundCardData }) {
const router = useRouter();
const [openAction, setOpenAction] = useState<Decision | null>(null);
const [note, setNote] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const paidPayment = refund.booking.payments.find(
(p) => p.status === "PAID" || p.status === "REFUNDED"
);
async function submit(decision: Decision) {
setError("");
setLoading(true);
const fd = new FormData();
fd.set("refundId", refund.id);
fd.set("decision", decision);
if (note.trim()) fd.set("adminNote", note);
const result = await decideRefundAction(fd);
setLoading(false);
if (result.error) {
setError(result.error);
return;
}
setOpenAction(null);
setNote("");
router.refresh();
}
return (
<article className="rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:p-6">
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-neutral-100 pb-4">
<div className="min-w-0">
<h3 className="truncate text-base font-bold text-neutral-900">
{refund.booking.trip.title}
</h3>
<p className="mt-0.5 text-xs text-neutral-500">
Dilaporkan {formatDate(refund.createdAt)} oleh{" "}
<span className="font-semibold">
{refund.reportedBy === "PARTICIPANT" ? "Peserta" : "Organizer"}
</span>
{" · "}
<span className="font-mono">{refund.id.slice(0, 8)}</span>
</p>
</div>
<StatusPill status={refund.status} />
</header>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
<Field
label="Peserta booking"
value={`${refund.booking.user.name} · ${refund.booking.user.email}`}
/>
<Field label="Booking ID" value={refund.booking.id} mono />
<Field
label="Tanggal trip"
value={formatDate(refund.booking.trip.date)}
/>
<Field
label="Alasan"
value={REASON_LABEL[refund.reason] ?? refund.reason}
/>
<Field
label="Nominal refund"
value={`${formatRupiah(refund.amount)} ${refund.currency !== "IDR" ? `(${refund.currency})` : ""}`}
highlight
/>
<Field
label="Total dibayar"
value={
paidPayment
? `${formatRupiah(paidPayment.amount)} · ${paidPayment.provider} ${paidPayment.method ?? ""}`
: "—"
}
/>
</div>
<div className="mt-4 rounded-xl bg-neutral-50 p-3 text-sm text-neutral-700">
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-neutral-500">
Isi laporan
</p>
<p className="whitespace-pre-wrap">{refund.reportNote}</p>
</div>
{refund.adminNote && (
<div className="mt-3 rounded-xl bg-blue-50 p-3 text-sm text-blue-800">
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-blue-600">
Catatan admin
</p>
<p className="whitespace-pre-wrap">{refund.adminNote}</p>
</div>
)}
{refund.reviewedBy && refund.reviewedAt && (
<p className="mt-3 text-xs text-neutral-500">
Diproses oleh {refund.reviewedBy.name} pada{" "}
{formatDate(refund.reviewedAt)}
{refund.succeededAt && ` · uang keluar ${formatDate(refund.succeededAt)}`}
{refund.failedAt && ` · gagal ${formatDate(refund.failedAt)}`}
</p>
)}
{(refund.status === "PENDING" || refund.status === "APPROVED") && (
<div className="mt-5 border-t border-neutral-100 pt-4">
{error && (
<div className="mb-3 rounded-lg bg-red-50 px-3 py-2 text-xs text-red-600">
{error}
</div>
)}
{openAction ? (
<ActionForm
decision={openAction}
note={note}
setNote={setNote}
loading={loading}
onCancel={() => {
setOpenAction(null);
setNote("");
setError("");
}}
onConfirm={() => submit(openAction)}
/>
) : (
<div className="flex flex-wrap gap-2">
{refund.status === "PENDING" && (
<>
<button
type="button"
onClick={() => setOpenAction("APPROVE")}
disabled={loading}
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
>
Setujui
</button>
<button
type="button"
onClick={() => setOpenAction("REJECT")}
disabled={loading}
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
>
Tolak
</button>
</>
)}
{refund.status === "APPROVED" && (
<>
<button
type="button"
onClick={() => setOpenAction("SUCCEEDED")}
disabled={loading}
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
>
💸 Tandai sudah ditransfer
</button>
<button
type="button"
onClick={() => setOpenAction("FAILED")}
disabled={loading}
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
>
Tandai gagal
</button>
</>
)}
</div>
)}
</div>
)}
</article>
);
}
function ActionForm({
decision,
note,
setNote,
loading,
onCancel,
onConfirm,
}: {
decision: Decision;
note: string;
setNote: (v: string) => void;
loading: boolean;
onCancel: () => void;
onConfirm: () => void;
}) {
const cfg = {
APPROVE: {
label: "Setujui Refund",
placeholder: "Catatan untuk approval (opsional)",
required: false,
btnLabel: "Konfirmasi Setuju",
btnClass: "bg-primary-600 hover:bg-primary-700",
},
REJECT: {
label: "Tolak Refund",
placeholder: "Alasan penolakan (wajib)",
required: true,
btnLabel: "Konfirmasi Tolak",
btnClass: "bg-red-600 hover:bg-red-700",
},
SUCCEEDED: {
label: "Tandai Sudah Transfer",
placeholder: "Referensi transfer / nomor mutasi bank (wajib)",
required: true,
btnLabel: "Tandai SUCCEEDED",
btnClass: "bg-primary-600 hover:bg-primary-700",
},
FAILED: {
label: "Tandai Gagal",
placeholder: "Alasan gagal (wajib)",
required: true,
btnLabel: "Tandai FAILED",
btnClass: "bg-red-600 hover:bg-red-700",
},
}[decision];
return (
<div className="space-y-2">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-600">
{cfg.label}
</p>
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
rows={2}
placeholder={cfg.placeholder}
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
/>
<div className="flex gap-2">
<button
type="button"
onClick={onConfirm}
disabled={loading || (cfg.required && !note.trim())}
className={`rounded-xl px-4 py-2 text-sm font-bold text-white disabled:opacity-50 ${cfg.btnClass}`}
>
{loading ? "Memproses…" : cfg.btnLabel}
</button>
<button
type="button"
onClick={onCancel}
disabled={loading}
className="rounded-xl border border-neutral-200 bg-white px-4 py-2 text-sm font-medium text-neutral-600 hover:bg-neutral-50 disabled:opacity-50"
>
Batal
</button>
</div>
</div>
);
}
function Field({
label,
value,
mono,
highlight,
}: {
label: string;
value: string;
mono?: boolean;
highlight?: boolean;
}) {
return (
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">
{label}
</p>
<p
className={`mt-0.5 text-sm ${mono ? "font-mono" : ""} ${
highlight ? "font-bold text-primary-700" : "text-neutral-800"
}`}
>
{value}
</p>
</div>
);
}
function StatusPill({ status }: { status: RefundStatus }) {
const cfg: Record<RefundStatus, { label: string; cls: string }> = {
PENDING: {
label: "Pending Review",
cls: "bg-amber-50 text-amber-700 ring-amber-200",
},
APPROVED: {
label: "Disetujui",
cls: "bg-blue-50 text-blue-700 ring-blue-200",
},
REJECTED: { label: "Ditolak", cls: "bg-red-50 text-red-700 ring-red-200" },
PROCESSING: {
label: "Diproses",
cls: "bg-violet-50 text-violet-700 ring-violet-200",
},
SUCCEEDED: {
label: "Selesai",
cls: "bg-primary-50 text-primary-700 ring-primary-200",
},
FAILED: { label: "Gagal", cls: "bg-red-50 text-red-700 ring-red-200" },
};
const c = cfg[status];
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ring-1 ${c.cls}`}
>
{c.label}
</span>
);
}
+57
View File
@@ -0,0 +1,57 @@
import { z } from "zod/v4";
import { LIMITS } from "@/lib/limits";
const reasonValues = [
"USER_CANCELLATION",
"ORGANIZER_CANCELLED",
"TRIP_ISSUE",
"ADMIN_ADJUSTMENT",
"DISPUTE_RESOLVED",
"OTHER",
] as const;
const reporterValues = ["PARTICIPANT", "ORGANIZER"] as const;
const refundNote = z
.string()
.trim()
.min(3, "Isi catatan minimal 3 karakter")
.max(
LIMITS.MAX_REFUND_NOTE_LENGTH,
`Catatan maksimal ${LIMITS.MAX_REFUND_NOTE_LENGTH} karakter`
);
export const createRefundSchema = z.object({
bookingId: z.string().trim().min(1, "Booking ID wajib"),
reason: z.enum(reasonValues, { error: "Alasan tidak valid" }),
reportedBy: z.enum(reporterValues, { error: "Pelapor tidak valid" }),
reportNote: refundNote,
/** Kosong = full remaining. Angka positif (IDR) untuk partial. */
amount: z
.string()
.trim()
.optional()
.transform((v) => (v && v.length > 0 ? Number(v.replace(/[^\d]/g, "")) : undefined))
.refine(
(n) => n === undefined || (Number.isInteger(n) && n > 0),
"Nominal harus bilangan bulat positif"
),
});
export const refundDecisionSchema = z.object({
refundId: z.string().trim().min(1, "Refund ID wajib"),
decision: z.enum(["APPROVE", "REJECT", "SUCCEEDED", "FAILED"], {
error: "Keputusan tidak valid",
}),
adminNote: z
.string()
.trim()
.max(
LIMITS.MAX_REFUND_NOTE_LENGTH,
`Catatan maksimal ${LIMITS.MAX_REFUND_NOTE_LENGTH} karakter`
)
.optional(),
});
export type CreateRefundInput = z.infer<typeof createRefundSchema>;
export type RefundDecisionInput = z.infer<typeof refundDecisionSchema>;
+24
View File
@@ -180,3 +180,27 @@ export async function rejectParticipantAction(
return { error: (err as Error).message };
}
}
export async function cancelTripAction(tripId: string) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return { error: "Kamu harus login terlebih dahulu" };
}
try {
const result = await tripService.closeTrip(tripId, session.user.id);
revalidatePath(`/trips/${tripId}`);
revalidatePath("/trips");
revalidatePath("/");
revalidatePath("/profile");
revalidatePath("/admin/refunds");
return {
success: true as const,
refundCount: result.refundsCreated.length,
cancelledCount: result.cancelledBookings.length,
skippedCount: result.skippedBookings.length,
};
} catch (err) {
return { error: (err as Error).message };
}
}
@@ -0,0 +1,143 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { cancelTripAction } from "@/features/trip/actions";
interface CancelTripButtonProps {
tripId: string;
/** Jumlah peserta dengan booking PAID — preview impact. */
paidParticipantCount: number;
}
export function CancelTripButton({
tripId,
paidParticipantCount,
}: CancelTripButtonProps) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [confirmText, setConfirmText] = useState("");
const [result, setResult] = useState<
| { refundCount: number; cancelledCount: number; skippedCount: number }
| null
>(null);
async function handleConfirm() {
setLoading(true);
setError("");
const res = await cancelTripAction(tripId);
setLoading(false);
if ("error" in res) {
setError(res.error ?? "Terjadi kesalahan");
return;
}
setResult({
refundCount: res.refundCount,
cancelledCount: res.cancelledCount,
skippedCount: res.skippedCount,
});
router.refresh();
}
if (result) {
return (
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
<p className="font-semibold">Trip dibatalkan.</p>
<ul className="mt-2 list-inside list-disc space-y-0.5 text-xs">
<li>{result.refundCount} refund dibuat (menunggu admin transfer)</li>
<li>
{result.cancelledCount} booking belum-bayar di-cancel langsung
</li>
{result.skippedCount > 0 && (
<li>
{result.skippedCount} booking di-skip (sudah punya refund aktif
admin akan handle manual)
</li>
)}
</ul>
</div>
);
}
if (!open) {
return (
<button
type="button"
onClick={() => setOpen(true)}
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50"
>
Batalkan Trip
</button>
);
}
const requireConfirm = paidParticipantCount > 0;
const canSubmit = !requireConfirm || confirmText.trim() === "BATAL";
return (
<div className="space-y-3 rounded-2xl border-2 border-red-200 bg-red-50 p-4 text-sm">
<div>
<p className="font-bold text-red-900">Yakin batalkan trip ini?</p>
<p className="mt-1 text-xs text-red-800/80">
Aksi ini <span className="font-semibold">tidak bisa di-undo</span>.
Trip akan ditandai CLOSED dan semua peserta dibatalkan.
{paidParticipantCount > 0 && (
<>
{" "}Sistem akan otomatis membuat{" "}
<span className="font-bold">
{paidParticipantCount} refund
</span>{" "}
full amount untuk peserta yang sudah membayar admin SeTrip akan
memproses transfer.
</>
)}
</p>
</div>
{requireConfirm && (
<label className="block">
<span className="text-xs font-semibold uppercase tracking-wide text-red-700">
Ketik <span className="font-mono">BATAL</span> untuk konfirmasi
</span>
<input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder="BATAL"
className="mt-1 w-full rounded-xl border border-red-300 bg-white px-3 py-2 font-mono text-sm focus:border-red-500 focus:outline-none"
/>
</label>
)}
{error && (
<div className="rounded-lg bg-white px-3 py-2 text-xs font-medium text-red-700">
{error}
</div>
)}
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={handleConfirm}
disabled={loading || !canSubmit}
className="rounded-xl bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700 disabled:opacity-50"
>
{loading ? "Memproses…" : "Ya, Batalkan Trip"}
</button>
<button
type="button"
onClick={() => {
setOpen(false);
setConfirmText("");
setError("");
}}
disabled={loading}
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-100 disabled:opacity-50"
>
Batal
</button>
</div>
</div>
);
}
+13 -7
View File
@@ -23,6 +23,9 @@ interface JoinTripButtonProps {
tripStatus: string;
/** Tanggal berangkat trip sudah lewat */
isDeparturePast?: boolean;
/** Sembunyikan tombol cancel dipakai saat booking PAID dan parent
* menampilkan CancelBookingButton (refund flow) di tempat terpisah. */
hideCancelButton?: boolean;
}
export function JoinTripButton({
@@ -36,6 +39,7 @@ export function JoinTripButton({
isFull,
tripStatus,
isDeparturePast,
hideCancelButton,
}: JoinTripButtonProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
@@ -163,13 +167,15 @@ export function JoinTripButton({
</Link>
)}
{isJoined ? (
<button
onClick={handleCancel}
disabled={loading}
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
>
{loading ? "Memproses..." : "Batal Ikut"}
</button>
hideCancelButton ? null : (
<button
onClick={handleCancel}
disabled={loading}
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
>
{loading ? "Memproses..." : "Batal Ikut"}
</button>
)
) : (
<button
onClick={handleJoin}
+2
View File
@@ -31,4 +31,6 @@ export const LIMITS = {
MAX_BANK_ACCOUNT_NUMBER_LENGTH: 32,
MAX_REJECTION_REASON_LENGTH: 500,
NIK_LENGTH: 16,
/** Catatan laporan dari peserta/organizer + catatan admin pada refund. */
MAX_REFUND_NOTE_LENGTH: 1000,
} as const;
+93
View File
@@ -0,0 +1,93 @@
/**
* Refund policy hardcoded untuk MVP (PR-R3). Akan jadi data-driven di PR-R5.
*
* Aturan: hitung persentase refund berdasarkan jarak hari ke tanggal berangkat
* (UTC calendar day). Selalu integer rupiah pakai Math.floor supaya tidak
* ada sub-rupiah dan total refund tidak pernah melebihi nominal yang dibayar.
*
* Tier:
* - 7 hari sebelum berangkat 80% refund (organizer ambil 20% admin fee)
* - 36 hari sebelum berangkat 50% refund
* - <3 hari sebelum berangkat / sudah lewat 0% (tidak ada refund)
*/
import { utcStartOfDay } from "@/lib/trip-dates";
export interface RefundTier {
/** Minimum jumlah hari sebelum berangkat untuk tier ini. */
minDaysBefore: number;
/** Persentase nominal yang di-refund (0100). */
refundPercentage: number;
/** Label untuk UI. */
label: string;
}
const TIERS: RefundTier[] = [
{ minDaysBefore: 7, refundPercentage: 80, label: "≥ 7 hari sebelum berangkat" },
{ minDaysBefore: 3, refundPercentage: 50, label: "36 hari sebelum berangkat" },
{ minDaysBefore: 0, refundPercentage: 0, label: "Kurang dari 3 hari / sudah lewat" },
];
export function getRefundPolicyTiers(): RefundTier[] {
return TIERS;
}
/**
* Jumlah hari kalender UTC dari sekarang ke tanggal berangkat. Negative kalau
* tanggal sudah lewat. Pakai start-of-day UTC supaya jam tidak mempengaruhi.
*/
export function daysUntilDeparture(
departureDate: Date,
now: Date = new Date()
): number {
const todayMs = utcStartOfDay(now).getTime();
const depMs = utcStartOfDay(departureDate).getTime();
const oneDayMs = 24 * 60 * 60 * 1000;
return Math.floor((depMs - todayMs) / oneDayMs);
}
/** Tier aktif untuk jumlah hari yang diberikan. */
export function getTierForDays(days: number): RefundTier {
for (const tier of TIERS) {
if (days >= tier.minDaysBefore) {
return tier;
}
}
return TIERS[TIERS.length - 1];
}
/**
* Hitung nominal refund (IDR integer) berdasarkan harga booking dan jarak ke
* tanggal berangkat. Floor supaya tidak pernah > bookingAmount.
*/
export function calculateRefundAmount(
bookingAmount: number,
days: number
): number {
if (bookingAmount <= 0) return 0;
const tier = getTierForDays(days);
return Math.floor((bookingAmount * tier.refundPercentage) / 100);
}
export interface RefundPreview {
days: number;
tier: RefundTier;
refundAmount: number;
bookingAmount: number;
}
/** Bundle lengkap untuk display di UI — preview cancel booking. */
export function previewRefund(
bookingAmount: number,
departureDate: Date,
now: Date = new Date()
): RefundPreview {
const days = daysUntilDeparture(departureDate, now);
const tier = getTierForDays(days);
return {
days,
tier,
refundAmount: calculateRefundAmount(bookingAmount, days),
bookingAmount,
};
}
@@ -0,0 +1,57 @@
-- AlterEnum
ALTER TYPE "BookingStatus" ADD VALUE 'PARTIALLY_REFUNDED';
-- CreateEnum
CREATE TYPE "RefundReason" AS ENUM ('USER_CANCELLATION', 'ORGANIZER_CANCELLED', 'TRIP_ISSUE', 'ADMIN_ADJUSTMENT', 'DISPUTE_RESOLVED', 'OTHER');
-- CreateEnum
CREATE TYPE "RefundStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED', 'PROCESSING', 'SUCCEEDED', 'FAILED');
-- CreateEnum
CREATE TYPE "RefundInitiator" AS ENUM ('USER', 'ORGANIZER', 'SYSTEM', 'ADMIN');
-- CreateEnum
CREATE TYPE "RefundReporter" AS ENUM ('PARTICIPANT', 'ORGANIZER');
-- CreateTable
CREATE TABLE "Refund" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"paymentId" TEXT,
"amount" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'IDR',
"reason" "RefundReason" NOT NULL,
"reportedBy" "RefundReporter" NOT NULL,
"reportNote" TEXT NOT NULL,
"initiatedBy" "RefundInitiator" NOT NULL DEFAULT 'ADMIN',
"status" "RefundStatus" NOT NULL DEFAULT 'PENDING',
"idempotencyKey" TEXT NOT NULL,
"adminNote" TEXT,
"reviewedById" TEXT,
"reviewedAt" TIMESTAMP(3),
"succeededAt" TIMESTAMP(3),
"failedAt" TIMESTAMP(3),
"externalRefundId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Refund_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Refund_idempotencyKey_key" ON "Refund"("idempotencyKey");
-- CreateIndex
CREATE INDEX "Refund_bookingId_status_idx" ON "Refund"("bookingId", "status");
-- CreateIndex
CREATE INDEX "Refund_status_createdAt_idx" ON "Refund"("status", "createdAt");
-- AddForeignKey
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_paymentId_fkey" FOREIGN KEY ("paymentId") REFERENCES "Payment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_reviewedById_fkey" FOREIGN KEY ("reviewedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+108
View File
@@ -32,6 +32,8 @@ model User {
organizerVerification OrganizerVerification? @relation("OrganizerVerificationOwner")
reviewedVerifications OrganizerVerification[] @relation("OrganizerVerificationReviewer")
reviewedRefunds Refund[] @relation("RefundReviewer")
profile UserProfile?
}
@@ -258,6 +260,7 @@ model Booking {
status BookingStatus @default(PENDING)
payments Payment[]
refunds Refund[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -275,6 +278,7 @@ enum BookingStatus {
PAID
CANCELLED
REFUNDED
PARTIALLY_REFUNDED
EXPIRED
}
@@ -307,6 +311,8 @@ model Payment {
failedAt DateTime?
rejectionReason String?
refunds Refund[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -328,3 +334,105 @@ enum PaymentStatus {
CANCELLED
REFUNDED
}
/// Refund = financial event terpisah dari Booking. Satu Booking bisa punya
/// banyak Refund (partial, multi-tahap). Setiap row auditable: kapan dibuat,
/// siapa melaporkan, siapa approve, kapan SUCCEEDED. Never delete — kalau
/// gagal, set status=FAILED + alasan.
///
/// Di MVP refund dimasukkan admin secara manual berdasarkan laporan dari
/// peserta atau organizer (via WhatsApp/email). Phase berikutnya akan
/// menambah self-service flow dari user dan organizer.
model Refund {
id String @id @default(cuid())
bookingId String
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
/// Payment yang di-refund. Opsional di MVP (manual transfer bisa tidak
/// terikat ke Payment row tertentu); wajib saat integrasi Midtrans (R-4).
paymentId String?
payment Payment? @relation(fields: [paymentId], references: [id], onDelete: Restrict)
/// Nominal refund dalam satuan terkecil (IDR rupiah, integer). Boleh < total
/// payment untuk partial. Service layer enforce SUM(SUCCEEDED) <= payment.amount.
amount Int
currency String @default("IDR")
reason RefundReason
/// Siapa yang melaporkan kebutuhan refund ini ke admin.
reportedBy RefundReporter
/// Isi laporan dari peserta/organizer yang admin terima (mis. WA, email).
reportNote String
/// Pihak yang membuat record di sistem. Di MVP selalu ADMIN; saat self-service
/// nanti USER/ORGANIZER, dan SYSTEM untuk auto-trigger dari trip dibatalkan.
initiatedBy RefundInitiator @default(ADMIN)
status RefundStatus @default(PENDING)
/// Idempotency key, dipakai saat panggil Midtrans Refund API di R-4. Generate
/// sekali saat create supaya retry gateway tidak double-refund.
idempotencyKey String @unique
/// Catatan admin: alasan tolak, referensi transfer manual, dst. Bebas teks.
adminNote String?
/// Admin yang terakhir mengubah status (approve/reject/mark-succeeded/failed).
reviewedById String?
reviewedBy User? @relation("RefundReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
reviewedAt DateTime?
succeededAt DateTime?
failedAt DateTime?
/// ID refund di gateway (mis. Midtrans refund_id). Kosong untuk manual transfer.
externalRefundId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([bookingId, status])
@@index([status, createdAt])
}
enum RefundReason {
/// Peserta cancel booking sendiri (mengikuti refund window policy).
USER_CANCELLATION
/// Organizer membatalkan trip — peserta dapat full refund.
ORGANIZER_CANCELLED
/// Masalah saat/setelah trip (mis. itinerary tidak sesuai).
TRIP_ISSUE
/// Penyesuaian dari admin (kompensasi, koreksi nominal, dll.).
ADMIN_ADJUSTMENT
/// Hasil resolusi dispute / chargeback bank.
DISPUTE_RESOLVED
OTHER
}
enum RefundStatus {
/// Baru dilaporkan, menunggu review admin.
PENDING
/// Admin sudah setujui, siap dieksekusi (manual transfer / gateway).
APPROVED
/// Admin tolak (alasan di `adminNote`).
REJECTED
/// (R-4) Request sudah dikirim ke gateway, menunggu callback.
PROCESSING
/// Uang sudah keluar dari kas Setrip / merchant gateway.
SUCCEEDED
/// Eksekusi gagal (alasan di `adminNote`). Record tidak dihapus.
FAILED
}
enum RefundInitiator {
USER
ORGANIZER
SYSTEM
ADMIN
}
enum RefundReporter {
PARTICIPANT
ORGANIZER
}
+13
View File
@@ -71,4 +71,17 @@ export const bookingRepo = {
data: { status },
});
},
/**
* Jumlah booking PAID/PARTIALLY_REFUNDED di trip. Dipakai untuk preview
* dampak cancel-trip (berapa peserta yang akan dapat auto-refund).
*/
async countSettledForTrip(tripId: string) {
return prisma.booking.count({
where: {
tripId,
status: { in: ["PAID", "PARTIALLY_REFUNDED"] },
},
});
},
};
+111
View File
@@ -0,0 +1,111 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@/app/generated/prisma/client";
const refundListInclude = {
booking: {
include: {
trip: { select: { id: true, title: true, date: true, organizerId: true } },
user: { select: { id: true, name: true, email: true, image: true } },
payments: {
orderBy: { createdAt: "desc" },
select: {
id: true,
provider: true,
method: true,
amount: true,
status: true,
paidAt: true,
},
},
},
},
payment: {
select: {
id: true,
provider: true,
method: true,
amount: true,
status: true,
},
},
reviewedBy: { select: { id: true, name: true, email: true } },
} satisfies Prisma.RefundInclude;
export const refundRepo = {
async findById(id: string) {
return prisma.refund.findUnique({
where: { id },
include: refundListInclude,
});
},
async listByStatus(
status?: "PENDING" | "APPROVED" | "REJECTED" | "PROCESSING" | "SUCCEEDED" | "FAILED"
) {
return prisma.refund.findMany({
where: status ? { status } : undefined,
orderBy: { createdAt: "desc" },
include: refundListInclude,
});
},
async listByBooking(bookingId: string) {
return prisma.refund.findMany({
where: { bookingId },
orderBy: { createdAt: "desc" },
});
},
/** Total nominal yang sudah SUCCEEDED untuk satu booking. Dipakai service untuk
* validasi `SUM(SUCCEEDED) + new.amount <= payment.amount`. */
async sumSucceededAmount(bookingId: string, tx?: Prisma.TransactionClient): Promise<number> {
const client = tx ?? prisma;
const agg = await client.refund.aggregate({
where: { bookingId, status: "SUCCEEDED" },
_sum: { amount: true },
});
return agg._sum.amount ?? 0;
},
/** Pending + approved + processing refund yang "in-flight" (belum settled).
* Dipakai untuk cek apakah booking masih punya refund aktif. */
async hasActiveRefund(bookingId: string, tx?: Prisma.TransactionClient): Promise<boolean> {
const client = tx ?? prisma;
const count = await client.refund.count({
where: {
bookingId,
status: { in: ["PENDING", "APPROVED", "PROCESSING"] },
},
});
return count > 0;
},
async create(
data: Pick<
Prisma.RefundUncheckedCreateInput,
| "bookingId"
| "paymentId"
| "amount"
| "reason"
| "reportedBy"
| "reportNote"
| "initiatedBy"
| "idempotencyKey"
>,
tx?: Prisma.TransactionClient
) {
const client = tx ?? prisma;
return client.refund.create({ data });
},
async update(
id: string,
data: Prisma.RefundUncheckedUpdateInput,
tx?: Prisma.TransactionClient
) {
const client = tx ?? prisma;
return client.refund.update({ where: { id }, data });
},
};
export type RefundWithRelations = Awaited<ReturnType<typeof refundRepo.findById>>;
+502
View File
@@ -0,0 +1,502 @@
import { randomBytes } from "crypto";
import { Prisma } from "@/app/generated/prisma/client";
import { prisma } from "@/lib/prisma";
import { refundRepo } from "@/server/repositories/refund.repo";
import { calculateRefundAmount, daysUntilDeparture } from "@/lib/refund-policy";
import { isTripDepartureDayPast } from "@/lib/trip-dates";
const SERIAL_TX_ATTEMPTS = 6;
function isSerializationConflict(err: unknown): boolean {
return (
typeof err === "object" &&
err !== null &&
"code" in err &&
(err as { code: string }).code === "P2034"
);
}
async function runSerializable<T>(fn: (tx: Prisma.TransactionClient) => Promise<T>): Promise<T> {
let lastErr: unknown;
for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
try {
return await prisma.$transaction(fn, {
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
maxWait: 5000,
timeout: 15000,
});
} catch (e) {
lastErr = e;
if (isSerializationConflict(e) && attempt < SERIAL_TX_ATTEMPTS - 1) {
continue;
}
throw e;
}
}
throw lastErr instanceof Error
? lastErr
: new Error("Gagal memproses refund. Coba lagi sebentar.");
}
function newIdempotencyKey(): string {
return `refund_${randomBytes(16).toString("hex")}`;
}
type RequestRefundInput = {
bookingId: string;
reason:
| "USER_CANCELLATION"
| "ORGANIZER_CANCELLED"
| "TRIP_ISSUE"
| "ADMIN_ADJUSTMENT"
| "DISPUTE_RESOLVED"
| "OTHER";
reportedBy: "PARTICIPANT" | "ORGANIZER";
reportNote: string;
/** Nominal refund (IDR). Kalau tidak diisi service akan pakai sisa
* refundable amount (payment.amount - sudah-di-refund). */
amount?: number;
/** Admin yang memasukkan laporan ke sistem. */
initiatedByAdminId: string;
};
export const refundService = {
/**
* Admin mencatat laporan refund dari peserta atau organizer ke sistem.
* Status awal: PENDING. Belum mengubah Booking/Payment status.
*
* Idempotency: kalau booking masih punya refund PENDING/APPROVED/PROCESSING,
* tolak admin harus selesaikan yang lama dulu (reject atau succeeded).
*/
async requestRefund(input: RequestRefundInput) {
return runSerializable(async (tx) => {
const booking = await tx.booking.findUnique({
where: { id: input.bookingId },
include: {
payments: {
where: { status: "PAID" },
orderBy: { paidAt: "desc" },
take: 1,
},
},
});
if (!booking) {
throw new Error("Booking tidak ditemukan");
}
if (booking.amount <= 0) {
throw new Error("Booking gratis — tidak ada nominal untuk di-refund");
}
if (booking.status === "CANCELLED" || booking.status === "EXPIRED") {
throw new Error(
"Booking sudah dibatalkan/expired — tidak ada pembayaran untuk di-refund"
);
}
if (booking.status === "REFUNDED") {
throw new Error("Booking sudah refund penuh");
}
const paidPayment = booking.payments[0];
if (!paidPayment) {
throw new Error(
"Tidak ada Payment dengan status PAID di booking ini — tidak bisa di-refund"
);
}
const hasActive = await refundRepo.hasActiveRefund(input.bookingId, tx);
if (hasActive) {
throw new Error(
"Booking ini masih punya refund yang sedang diproses. Selesaikan dulu sebelum membuat yang baru."
);
}
const alreadyRefunded = await refundRepo.sumSucceededAmount(input.bookingId, tx);
const remaining = paidPayment.amount - alreadyRefunded;
if (remaining <= 0) {
throw new Error("Seluruh nominal sudah di-refund");
}
const amount = input.amount ?? remaining;
if (!Number.isInteger(amount) || amount <= 0) {
throw new Error("Nominal refund harus bilangan bulat positif");
}
if (amount > remaining) {
throw new Error(
`Nominal refund (Rp ${amount.toLocaleString("id-ID")}) melebihi sisa yang bisa di-refund (Rp ${remaining.toLocaleString("id-ID")})`
);
}
return refundRepo.create(
{
bookingId: input.bookingId,
paymentId: paidPayment.id,
amount,
reason: input.reason,
reportedBy: input.reportedBy,
reportNote: input.reportNote,
initiatedBy: "ADMIN",
idempotencyKey: newIdempotencyKey(),
},
tx
);
});
},
/** PENDING → APPROVED. Boleh menambah catatan admin (opsional). */
async approveRefund(input: { refundId: string; adminId: string; adminNote?: string }) {
return runSerializable(async (tx) => {
const refund = await tx.refund.findUnique({ where: { id: input.refundId } });
if (!refund) {
throw new Error("Refund tidak ditemukan");
}
if (refund.status !== "PENDING") {
throw new Error("Hanya refund berstatus PENDING yang bisa disetujui");
}
return refundRepo.update(
input.refundId,
{
status: "APPROVED",
reviewedById: input.adminId,
reviewedAt: new Date(),
adminNote: input.adminNote ?? refund.adminNote,
},
tx
);
});
},
/** PENDING → REJECTED. Alasan wajib supaya audit trail jelas. */
async rejectRefund(input: { refundId: string; adminId: string; adminNote: string }) {
if (!input.adminNote.trim()) {
throw new Error("Alasan tolak wajib diisi");
}
return runSerializable(async (tx) => {
const refund = await tx.refund.findUnique({ where: { id: input.refundId } });
if (!refund) {
throw new Error("Refund tidak ditemukan");
}
if (refund.status !== "PENDING") {
throw new Error("Hanya refund berstatus PENDING yang bisa ditolak");
}
return refundRepo.update(
input.refundId,
{
status: "REJECTED",
reviewedById: input.adminId,
reviewedAt: new Date(),
adminNote: input.adminNote.trim(),
},
tx
);
});
},
/**
* APPROVED SUCCEEDED untuk manual transfer (admin sudah transfer manual
* ke rekening peserta). adminNote diharapkan berisi referensi transfer.
*
* Side effects:
* - Update Payment.status REFUNDED hanya saat full refund.
* - Update Booking.status REFUNDED (full) atau PARTIALLY_REFUNDED (partial).
* - Untuk USER_CANCELLATION: bebaskan slot set TripParticipant CANCELLED
* dan re-open Trip (FULL OPEN) kalau peserta aktif < maxParticipants.
* Untuk ORGANIZER_CANCELLED slot tidak perlu dibebaskan (trip sudah CLOSED).
*/
async markSucceededManual(input: {
refundId: string;
adminId: string;
adminNote: string;
}) {
if (!input.adminNote.trim()) {
throw new Error("Catatan/referensi transfer wajib diisi");
}
return runSerializable(async (tx) => {
const refund = await tx.refund.findUnique({
where: { id: input.refundId },
});
if (!refund) {
throw new Error("Refund tidak ditemukan");
}
if (refund.status !== "APPROVED") {
throw new Error(
"Hanya refund APPROVED yang bisa ditandai SUCCEEDED. Setujui dulu."
);
}
const now = new Date();
await refundRepo.update(
input.refundId,
{
status: "SUCCEEDED",
succeededAt: now,
reviewedById: input.adminId,
reviewedAt: now,
adminNote: input.adminNote.trim(),
},
tx
);
const totalRefunded = await refundRepo.sumSucceededAmount(
refund.bookingId,
tx
);
if (refund.paymentId) {
const payment = await tx.payment.findUnique({
where: { id: refund.paymentId },
});
if (payment && totalRefunded >= payment.amount) {
await tx.payment.update({
where: { id: payment.id },
data: { status: "REFUNDED" },
});
}
}
const booking = await tx.booking.findUnique({
where: { id: refund.bookingId },
include: {
trip: { select: { id: true, status: true, maxParticipants: true } },
payments: {
where: { status: { in: ["PAID", "REFUNDED"] } },
orderBy: { paidAt: "desc" },
take: 1,
},
},
});
if (!booking) {
throw new Error("Booking tidak ditemukan saat menutup refund");
}
const paid = booking.payments[0];
if (paid) {
const nextStatus =
totalRefunded >= paid.amount ? "REFUNDED" : "PARTIALLY_REFUNDED";
if (booking.status !== nextStatus) {
await tx.booking.update({
where: { id: booking.id },
data: { status: nextStatus },
});
}
}
// Slot release untuk user cancellation. Organizer cancel di-handle
// closeTrip (participant + trip sudah di-CANCELLED/CLOSED di sana).
if (refund.reason === "USER_CANCELLATION") {
await tx.tripParticipant.updateMany({
where: {
id: booking.participantId,
status: { not: "CANCELLED" },
},
data: {
status: "CANCELLED",
markedPaidAt: null,
paymentConfirmedAt: null,
},
});
if (booking.trip.status === "FULL") {
const remaining = await tx.tripParticipant.count({
where: { tripId: booking.tripId, status: { not: "CANCELLED" } },
});
if (remaining < booking.trip.maxParticipants) {
await tx.trip.update({
where: { id: booking.tripId },
data: { status: "OPEN" },
});
}
}
}
return { ok: true as const };
});
},
/**
* Peserta cancel booking sendiri. Hitung refund pakai policy default
* (lib/refund-policy.ts) hardcoded MVP, akan jadi data-driven di R-5.
*
* Behaviour:
* - Kalau hasil hitung = 0 (di luar window): cancel participant + booking
* langsung, tanpa Refund row (uang tidak balik).
* - Kalau hasil hitung > 0: buat Refund PENDING (initiatedBy=USER,
* reportedBy=PARTICIPANT, reason=USER_CANCELLATION). Participant + booking
* TETAP CONFIRMED/PAID sampai admin mark SUCCEEDED slot baru bebas saat
* refund tuntas. Cegah double-request via hasActiveRefund.
*/
async requestUserCancellation(input: {
bookingId: string;
userId: string;
}) {
return runSerializable(async (tx) => {
const booking = await tx.booking.findUnique({
where: { id: input.bookingId },
include: {
trip: {
select: {
id: true,
date: true,
status: true,
maxParticipants: true,
},
},
payments: {
where: { status: "PAID" },
orderBy: { paidAt: "desc" },
take: 1,
},
},
});
if (!booking) {
throw new Error("Booking tidak ditemukan");
}
if (booking.userId !== input.userId) {
throw new Error("Booking ini bukan milikmu");
}
if (booking.status !== "PAID") {
throw new Error(
"Hanya booking PAID yang bisa cancel dengan refund. Booking yang belum lunas bisa cancel dari tombol 'Batal Ikut'."
);
}
if (isTripDepartureDayPast(booking.trip.date)) {
throw new Error(
"Trip sudah lewat tanggal berangkat — pembatalan ditutup"
);
}
const paid = booking.payments[0];
if (!paid) {
throw new Error(
"Tidak ada Payment dengan status PAID di booking ini"
);
}
const days = daysUntilDeparture(booking.trip.date);
const refundAmount = calculateRefundAmount(paid.amount, days);
if (refundAmount === 0) {
await tx.tripParticipant.update({
where: { id: booking.participantId },
data: {
status: "CANCELLED",
markedPaidAt: null,
paymentConfirmedAt: null,
},
});
await tx.booking.update({
where: { id: booking.id },
data: { status: "CANCELLED" },
});
if (booking.trip.status === "FULL") {
const remaining = await tx.tripParticipant.count({
where: { tripId: booking.tripId, status: { not: "CANCELLED" } },
});
if (remaining < booking.trip.maxParticipants) {
await tx.trip.update({
where: { id: booking.tripId },
data: { status: "OPEN" },
});
}
}
return {
kind: "CANCELLED_NO_REFUND" as const,
days,
refundAmount: 0,
};
}
const hasActive = await refundRepo.hasActiveRefund(input.bookingId, tx);
if (hasActive) {
throw new Error(
"Booking ini sudah punya refund yang sedang diproses"
);
}
const percentage = Math.floor((refundAmount * 100) / paid.amount);
const refund = await refundRepo.create(
{
bookingId: booking.id,
paymentId: paid.id,
amount: refundAmount,
reason: "USER_CANCELLATION",
reportedBy: "PARTICIPANT",
reportNote: `Self-service cancel oleh peserta — H-${days} dari tanggal berangkat (refund ${percentage}%).`,
initiatedBy: "USER",
idempotencyKey: newIdempotencyKey(),
},
tx
);
return {
kind: "REFUND_PENDING" as const,
refundId: refund.id,
days,
refundAmount,
};
});
},
/**
* Dipanggil dari tripService.closeTrip (organizer cancel trip) dengan tx
* yang sama. Buat Refund auto-approved untuk satu booking PAID. Tidak
* mengecek hasActiveRefund (caller harus filter dulu) supaya batch closeTrip
* idempotent dengan retry-safe.
*
* Refund langsung APPROVED policy jelas (organizer cancel = 100% refund),
* tapi eksekusi (SUCCEEDED) tetap manual oleh admin.
*/
async createSystemRefundForClosedTrip(
tx: Prisma.TransactionClient,
input: {
bookingId: string;
paymentId: string;
amount: number;
}
) {
const now = new Date();
return tx.refund.create({
data: {
bookingId: input.bookingId,
paymentId: input.paymentId,
amount: input.amount,
reason: "ORGANIZER_CANCELLED",
reportedBy: "ORGANIZER",
reportNote: "Organizer membatalkan trip — auto-create oleh SYSTEM.",
initiatedBy: "SYSTEM",
idempotencyKey: newIdempotencyKey(),
status: "APPROVED",
reviewedAt: now,
adminNote: "Auto-approved (SYSTEM): organizer cancel = full refund.",
},
});
},
/**
* APPROVED/PROCESSING FAILED. Catatan wajib (alasan gagal).
* Tidak mengubah Booking/Payment uang belum keluar.
*/
async markFailed(input: { refundId: string; adminId: string; adminNote: string }) {
if (!input.adminNote.trim()) {
throw new Error("Alasan gagal wajib diisi");
}
return runSerializable(async (tx) => {
const refund = await tx.refund.findUnique({ where: { id: input.refundId } });
if (!refund) {
throw new Error("Refund tidak ditemukan");
}
if (refund.status !== "APPROVED" && refund.status !== "PROCESSING") {
throw new Error(
"Hanya refund APPROVED atau PROCESSING yang bisa ditandai FAILED"
);
}
return refundRepo.update(
input.refundId,
{
status: "FAILED",
failedAt: new Date(),
reviewedById: input.adminId,
reviewedAt: new Date(),
adminNote: input.adminNote.trim(),
},
tx
);
});
},
};
+174
View File
@@ -5,6 +5,7 @@ import { tripRepo, type TripFilters } from "@/server/repositories/trip.repo";
import { participantRepo } from "@/server/repositories/participant.repo";
import { bookingRepo } from "@/server/repositories/booking.repo";
import { bookingService } from "@/server/services/booking.service";
import { refundService } from "@/server/services/refund.service";
import { LIMITS } from "@/lib/limits";
import { utcStartOfDay, isTripDepartureDayPast } from "@/lib/trip-dates";
import { isFreeTrip } from "@/lib/trip-pricing";
@@ -253,6 +254,19 @@ export const tripService = {
throw new Error("Kamu tidak terdaftar di trip ini");
}
// Safety: kalau booking sudah PAID, paksa lewat refund flow supaya tidak
// ada uang menggantung tanpa Refund record.
const existingBooking = await bookingRepo.findByTripAndUser(tripId, userId);
if (
existingBooking &&
(existingBooking.status === "PAID" ||
existingBooking.status === "PARTIALLY_REFUNDED")
) {
throw new Error(
"Booking kamu sudah lunas — pakai tombol 'Cancel & Request Refund' supaya uang bisa dikembalikan."
);
}
const result = await prisma.$transaction(async (tx) => {
const cancelled = await tx.tripParticipant.update({
where: { tripId_userId: { tripId, userId } },
@@ -422,4 +436,164 @@ export const tripService = {
return bookingService.confirmPaidManual(booking.id, organizerId);
},
/**
* Organizer batalkan trip (Trip.status = CLOSED). Atomic dalam satu
* serializable transaction:
* - Set Trip.status = CLOSED.
* - Untuk setiap peserta aktif:
* - Booking PAID buat Refund ORGANIZER_CANCELLED (auto-approved, full
* amount). Booking tetap PAID sampai admin mark SUCCEEDED jejak
* finansial harus terjaga.
* - Booking PENDING/AWAITING_PAY set CANCELLED langsung (uang belum
* masuk, tidak ada refund).
* - Booking PARTIALLY_REFUNDED / dengan refund aktif di-skip (admin
* handle manual supaya tidak double-refund).
* - Semua TripParticipant aktif CANCELLED.
*
* Idempotent: trip yang sudah CLOSED/COMPLETED akan ditolak supaya tidak
* dobel-buat refund.
*/
async closeTrip(tripId: string, organizerId: string) {
let lastErr: unknown;
for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
try {
return await prisma.$transaction(
async (tx) => {
const trip = await tx.trip.findUnique({
where: { id: tripId },
select: { id: true, status: true, organizerId: true, date: true },
});
if (!trip) {
throw new Error("Trip tidak ditemukan");
}
if (trip.organizerId !== organizerId) {
throw new Error(
"Hanya organizer trip ini yang bisa membatalkan trip"
);
}
if (trip.status === "CLOSED") {
throw new Error("Trip sudah dibatalkan");
}
if (trip.status === "COMPLETED") {
throw new Error(
"Trip sudah selesai (COMPLETED) — tidak bisa dibatalkan"
);
}
if (isTripDepartureDayPast(trip.date)) {
throw new Error(
"Tanggal berangkat sudah lewat — gunakan flow pelaporan biasa ke admin"
);
}
const bookings = await tx.booking.findMany({
where: { tripId },
include: {
payments: {
where: { status: "PAID" },
orderBy: { paidAt: "desc" },
take: 1,
},
refunds: {
where: {
status: { in: ["PENDING", "APPROVED", "PROCESSING"] },
},
select: { id: true },
},
},
});
const refundsCreated: string[] = [];
const cancelledBookings: string[] = [];
const skippedBookings: string[] = [];
for (const b of bookings) {
if (b.status === "CANCELLED" || b.status === "EXPIRED") {
continue;
}
if (b.status === "REFUNDED") {
continue;
}
if (b.refunds.length > 0) {
// Sudah ada refund aktif (mis. user request cancel). Admin
// handle manual supaya tidak konflik dengan refund existing.
skippedBookings.push(b.id);
continue;
}
if (b.status === "PAID" || b.status === "PARTIALLY_REFUNDED") {
const paid = b.payments[0];
if (!paid) {
// Payment tidak konsisten dgn booking status — skip + flag.
skippedBookings.push(b.id);
continue;
}
// Untuk PARTIALLY_REFUNDED, hitung sisa refundable.
const alreadyRefunded = await tx.refund.aggregate({
where: { bookingId: b.id, status: "SUCCEEDED" },
_sum: { amount: true },
});
const remaining = paid.amount - (alreadyRefunded._sum.amount ?? 0);
if (remaining <= 0) {
continue;
}
const refund = await refundService.createSystemRefundForClosedTrip(
tx,
{
bookingId: b.id,
paymentId: paid.id,
amount: remaining,
}
);
refundsCreated.push(refund.id);
} else {
// PENDING / AWAITING_PAY → uang belum masuk → langsung CANCELLED.
await tx.booking.update({
where: { id: b.id },
data: { status: "CANCELLED" },
});
cancelledBookings.push(b.id);
}
}
// Semua participant aktif → CANCELLED (apapun status booking-nya).
await tx.tripParticipant.updateMany({
where: { tripId, status: { not: "CANCELLED" } },
data: {
status: "CANCELLED",
markedPaidAt: null,
paymentConfirmedAt: null,
},
});
await tx.trip.update({
where: { id: tripId },
data: { status: "CLOSED" },
});
return {
ok: true as const,
refundsCreated,
cancelledBookings,
skippedBookings,
};
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
maxWait: 5000,
timeout: 15000,
}
);
} catch (e) {
lastErr = e;
if (isSerializationConflict(e) && attempt < SERIAL_TX_ATTEMPTS - 1) {
continue;
}
throw e;
}
}
throw lastErr instanceof Error
? lastErr
: new Error("Gagal membatalkan trip. Coba lagi sebentar.");
},
};