From 398eb0a2dfda031e3fe1b5592fe87f0ac5fcec62 Mon Sep 17 00:00:00 2001 From: Catherine Angel Date: Thu, 16 May 2024 14:07:53 +0700 Subject: [PATCH 1/7] [RED] update test for payment with transaction history --- .../payment/payment-details-with-url.test.tsx | 26 ++-- __tests__/payment/payment-fail.test.tsx | 72 +++++++++++ __tests__/payment/payment-success.test.tsx | 33 ++--- __tests__/profile/profile.test.tsx | 122 +++++++++++++++++- 4 files changed, 224 insertions(+), 29 deletions(-) create mode 100644 __tests__/payment/payment-fail.test.tsx diff --git a/__tests__/payment/payment-details-with-url.test.tsx b/__tests__/payment/payment-details-with-url.test.tsx index 0c994bf..2fa5b36 100644 --- a/__tests__/payment/payment-details-with-url.test.tsx +++ b/__tests__/payment/payment-details-with-url.test.tsx @@ -8,17 +8,19 @@ jest.mock('next/navigation', () => ({ })), })) +const mockTransaction = { + package: { name: 'Test Package' }, + payment_type: 'Test Payment Type', + payment_merchant: 'Test Payment Merchant', + midtrans_url: 'https://example.com/midtrans', +} + jest.mock('@/redux/api/paymentApi', () => ({ useLazyGetTransactionQuery: jest.fn(() => [ jest.fn(), { data: { - transaction_detail: { - package: { name: 'Test Package' }, - payment_type: 'Test Payment Type', - payment_merchant: 'Test Payment Merchant', - midtrans_url: 'https://example.com/midtrans', - }, + transaction_detail: mockTransaction, }, isLoading: false, }, @@ -36,10 +38,16 @@ describe('Payment Detail Page With Midtrans Url', () => { expect(queryByTestId('loader')).not.toBeInTheDocument() }) - expect(getByText('Transaction Detail')).toBeInTheDocument() + expect(getByText('Transaction detail')).toBeInTheDocument() expect(getByText('Test Package')).toBeInTheDocument() - expect(getByText('Test Payment Type')).toBeInTheDocument() - expect(getByText('Test Payment Merchant')).toBeInTheDocument() + expect(getByText('Payment Type')).toBeInTheDocument() + expect(getByText('Merchant')).toBeInTheDocument() + expect( + getByText(mockTransaction.payment_type.toUpperCase()) + ).toBeInTheDocument() + expect( + getByText(mockTransaction.payment_merchant.toUpperCase()) + ).toBeInTheDocument() fireEvent.click(getByText('Continue Transaction')) diff --git a/__tests__/payment/payment-fail.test.tsx b/__tests__/payment/payment-fail.test.tsx new file mode 100644 index 0000000..dac830d --- /dev/null +++ b/__tests__/payment/payment-fail.test.tsx @@ -0,0 +1,72 @@ +import { render, waitFor } from '@testing-library/react' +import '@testing-library/jest-dom' +import { useLazyGetTransactionQuery } from '@/redux/api/paymentApi' +import PaymentPage from '@/app/payment/page' + +jest.mock('next/link', () => { + const MockedLink = ({ children, href }: any) => {children} + + MockedLink.displayName = 'MockedNextLink' + + return MockedLink +}) + +jest.mock('next/navigation', () => ({ + useSearchParams: jest.fn(() => ({ + get: jest.fn((param) => { + switch (param) { + case 'order_id': + return 'test_order_id' + case 'transaction_status': + return 'deny' + default: + return null + } + }), + })), +})) + +jest.mock('@/redux/api/paymentApi', () => ({ + useLazyGetTransactionQuery: jest.fn(), +})) +describe('PaymentSuccess Component', () => { + it('renders the success message when data is loaded', async () => { + ;(useLazyGetTransactionQuery as jest.Mock).mockReturnValue([ + jest.fn(), + { + data: { + transaction_detail: { + id: 'transaction_id', + package: { + id: 2, + name: 'Premium', + }, + midtrans_url: 'http:example.com', + midtrans_transaction_id: 'midtrans_id', + order_id: 'order_id', + price: 10000, + checkout_time: '2024-05-06T14:49:19Z', + expiry_time: '2024-05-06T15:04:19Z', + payment_type: 'qris', + payment_merchant: 'gopay', + status: 'deny', + }, + }, + isLoading: false, + }, + ]) + + const { getByText, getByRole } = render() + + await waitFor(() => { + expect(getByText('Payment Failed')).toBeInTheDocument() + const button = getByRole('button', { name: 'See Transaction History' }) + expect(button.closest('a')).toHaveAttribute( + 'href', + '/profile?tab=history' + ) + }) + }) + + +}) diff --git a/__tests__/payment/payment-success.test.tsx b/__tests__/payment/payment-success.test.tsx index 33001b0..87b67be 100644 --- a/__tests__/payment/payment-success.test.tsx +++ b/__tests__/payment/payment-success.test.tsx @@ -1,7 +1,7 @@ import { render, waitFor } from '@testing-library/react' import '@testing-library/jest-dom' -import PaymentSuccessPage from '@/app/payment/success/page' import { useLazyGetTransactionQuery } from '@/redux/api/paymentApi' +import PaymentPage from '@/app/payment/page' jest.mock('next/link', () => { const MockedLink = ({ children, href }: any) => {children} @@ -13,7 +13,16 @@ jest.mock('next/link', () => { jest.mock('next/navigation', () => ({ useSearchParams: jest.fn(() => ({ - get: jest.fn(() => 'test_order_id'), + get: jest.fn((param) => { + switch (param) { + case 'order_id': + return 'test_order_id' + case 'transaction_status': + return 'settlement' + default: + return null + } + }), })), })) @@ -47,30 +56,16 @@ describe('PaymentSuccess Component', () => { }, ]) - const { getByText, getByRole } = render() + const { getByText, getByRole } = render() await waitFor(() => { expect(getByText('Payment Success')).toBeInTheDocument() expect(getByText('Premium')).toBeInTheDocument() - const button = getByRole('button', { name: 'Go to transactions' }) + const button = getByRole('button', { name: 'See Transaction History' }) expect(button.closest('a')).toHaveAttribute( 'href', - '/payment/transactions/' + '/profile?tab=history' ) }) }) - - it('shows loader when data is still loading', () => { - ;(useLazyGetTransactionQuery as jest.Mock).mockReturnValue([ - jest.fn(), - { - data: null, - isLoading: true, - }, - ]) - - const { getByTestId } = render() - - expect(getByTestId('loader')).toBeInTheDocument() - }) }) diff --git a/__tests__/profile/profile.test.tsx b/__tests__/profile/profile.test.tsx index f6a2f7e..53decae 100644 --- a/__tests__/profile/profile.test.tsx +++ b/__tests__/profile/profile.test.tsx @@ -1,11 +1,24 @@ import '@testing-library/jest-dom' import React from 'react' -import { render, screen, fireEvent } from '@testing-library/react' +import { + render, + screen, + fireEvent, + getAllByAltText, +} from '@testing-library/react' import { Provider } from 'react-redux' import { store } from '@/redux/store' import { useGetProfileQuery, useGetEventsQuery } from '@/redux/api/profileApi' import Profile from '@/app/profile/page' import { useGetSubscriptionsQuery } from '@/redux/api/subscriptionApi' +import { useGetTransactionListQuery } from '@/redux/api/paymentApi' +import dayjs from 'dayjs' + +jest.mock('next/navigation', () => ({ + useSearchParams: jest.fn(() => ({ + get: jest.fn(() => undefined), + })), +})) const mockEventsData = [ { @@ -19,6 +32,53 @@ const mockEventsData = [ services: 'Live Music, Food Stalls, Security', }, ] +const mockTransactionData = [ + { + id: 'mock-id-1', + package: { + name: 'Premium', + }, + midtrans_url: null, + midtrans_transaction_id: 'mid-1', + order_id: 'ord-1', + price: 10000, + checkout_time: '2024-05-06T14:49:19Z', + expiry_time: '2024-05-06T15:04:19Z', + payment_type: 'qris', + payment_merchant: 'gopay', + status: 'settlement', + }, + { + id: 'mock-id-2', + package: { + name: 'Premium', + }, + midtrans_url: null, + midtrans_transaction_id: 'mid-2', + order_id: 'ord-2', + price: 10000, + checkout_time: '2024-05-06T14:49:19Z', + expiry_time: '2024-05-06T15:04:19Z', + payment_type: 'qris', + payment_merchant: 'gopay', + status: 'failed', + }, + { + id: 'mock-id-3', + package: { + name: 'Premium', + }, + midtrans_url: null, + midtrans_transaction_id: 'mid-3', + order_id: 'ord-3', + price: 10000, + checkout_time: '2024-05-06T14:49:19Z', + expiry_time: '2024-05-06T15:04:19Z', + payment_type: 'qris', + payment_merchant: 'gopay', + status: 'pending', + }, +] jest.mock('@/redux/api/subscriptionApi', () => ({ useGetSubscriptionsQuery: jest.fn().mockReturnValue({ @@ -53,6 +113,10 @@ jest.mock('@/redux/api/profileApi', () => ({ useGetEventsQuery: jest.fn(), })) +jest.mock('@/redux/api/paymentApi', () => ({ + useGetTransactionListQuery: jest.fn(), +})) + Object.defineProperty(window, 'location', { value: { pathname: '/mock-path' }, }) @@ -75,6 +139,10 @@ describe('Profile Component', () => { isLoading: false, isError: false, }) + ;(useGetTransactionListQuery as jest.Mock).mockReturnValue({ + data: mockTransactionData, + isLoading: false, + }) }) it('displays error state correctly', () => { @@ -190,4 +258,56 @@ describe('Profile Component', () => { ) }) + + it('displays transaction information when there is at least one transaction', () => { + const myInitialState = 'history' + React.useState = jest.fn().mockReturnValue([myInitialState, {}]) + + const { getByText, getAllByText } = render( + + + + ) + + expect(getByText('Checkout Time')).toBeInTheDocument() + expect(getByText('Package Plan')).toBeInTheDocument() + expect(getByText('Price')).toBeInTheDocument() + expect(getByText('Expiry Time')).toBeInTheDocument() + expect(getByText('Status')).toBeInTheDocument() + expect(getByText(mockTransactionData[0].status)).toBeInTheDocument() + expect(getByText(mockTransactionData[0].price)).toBeInTheDocument() + expect( + getByText( + dayjs(mockTransactionData[0].checkout_time).format( + 'ddd, D MMM YY HH:mm' + ) + ) + ).toBeInTheDocument() + expect( + getByText( + dayjs(mockTransactionData[0].expiry_time).format('ddd, D MMM YY HH:mm') + ) + ).toBeInTheDocument() + }) + + it('displays no transaction information', () => { + const myInitialState = 'history' + React.useState = jest.fn().mockReturnValue([myInitialState, {}]) + ;(useGetTransactionListQuery as jest.Mock).mockReturnValue({ + data: [], + isLoading: false, + }) + const { getByText } = render( + + + + ) + + expect(getByText('Checkout Time')).toBeInTheDocument() + expect(getByText('Package Plan')).toBeInTheDocument() + expect(getByText('Price')).toBeInTheDocument() + expect(getByText('Expiry Time')).toBeInTheDocument() + expect(getByText('Status')).toBeInTheDocument() + expect(getByText('No transactions recorded')).toBeInTheDocument() + }) }) From f3d3387e3eda7b4d2fde09743535cd8598323b29 Mon Sep 17 00:00:00 2001 From: Catherine Angel Date: Thu, 16 May 2024 14:08:49 +0700 Subject: [PATCH 2/7] [RED] update test suited for payment status page --- __tests__/payment/payment-fail.test.tsx | 2 - src/app/payment/success/PaymentSuccess.tsx | 45 ---------------------- src/app/payment/success/page.tsx | 4 -- 3 files changed, 51 deletions(-) delete mode 100644 src/app/payment/success/PaymentSuccess.tsx delete mode 100644 src/app/payment/success/page.tsx diff --git a/__tests__/payment/payment-fail.test.tsx b/__tests__/payment/payment-fail.test.tsx index dac830d..afe2f85 100644 --- a/__tests__/payment/payment-fail.test.tsx +++ b/__tests__/payment/payment-fail.test.tsx @@ -67,6 +67,4 @@ describe('PaymentSuccess Component', () => { ) }) }) - - }) diff --git a/src/app/payment/success/PaymentSuccess.tsx b/src/app/payment/success/PaymentSuccess.tsx deleted file mode 100644 index 54fff71..0000000 --- a/src/app/payment/success/PaymentSuccess.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'use client' - -import { Button } from '@/components/elements/Button' -import { useLazyGetTransactionQuery } from '@/redux/api/paymentApi' -import Link from 'next/link' -import { useSearchParams } from 'next/navigation' -import { useEffect } from 'react' - -export const PaymentSuccess = () => { - const searchParams = useSearchParams() - const orderId = searchParams.get('order_id') - const [getTransaction, { data, isLoading }] = useLazyGetTransactionQuery() - useEffect(() => { - if (orderId) { - getTransaction({ order_id: orderId }) - } - }, [orderId]) - - return ( -
- {isLoading ? ( -
-
-
- ) : ( -
-

- Payment Success -

-

- Enjoy the{' '} - - {data?.transaction_detail.package.name} - {' '} - Package -

- - - - -
- )} -
- ) -} diff --git a/src/app/payment/success/page.tsx b/src/app/payment/success/page.tsx deleted file mode 100644 index 3c16314..0000000 --- a/src/app/payment/success/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { PaymentSuccess } from './PaymentSuccess' -export default function PaymentSuccessPage() { - return -} From 01d09709c35baf45d2af04883ec08718a2dd5b23 Mon Sep 17 00:00:00 2001 From: Catherine Angel Date: Thu, 16 May 2024 14:10:48 +0700 Subject: [PATCH 3/7] [GREEN] implement transaction history --- src/app/payment/PaymentDetail.tsx | 39 ++++++- src/app/payment/PaymentError.tsx | 27 +++++ src/app/payment/PaymentSuccess.tsx | 32 ++++++ src/app/profile/SubscriptionHistory.tsx | 12 +-- src/app/profile/TransactionHistory.tsx | 131 +++++++++++++++++++++++ src/app/profile/constant.ts | 25 +++++ src/app/profile/page.tsx | 19 +++- src/components/elements/Button/index.tsx | 9 +- src/types/payment.ts | 22 ++++ src/types/subscription.ts | 1 + src/utils/getTransactionStatus.ts | 23 ++++ 11 files changed, 321 insertions(+), 19 deletions(-) create mode 100644 src/app/payment/PaymentError.tsx create mode 100644 src/app/payment/PaymentSuccess.tsx create mode 100644 src/app/profile/TransactionHistory.tsx create mode 100644 src/utils/getTransactionStatus.ts diff --git a/src/app/payment/PaymentDetail.tsx b/src/app/payment/PaymentDetail.tsx index b66de4d..665d0c3 100644 --- a/src/app/payment/PaymentDetail.tsx +++ b/src/app/payment/PaymentDetail.tsx @@ -1,18 +1,34 @@ 'use client' import { Button } from '@/components/elements/Button' import { useLazyGetTransactionQuery } from '@/redux/api/paymentApi' +import { transactionErrorStatuses, TransactionStatus } from '@/types/payment' import { useSearchParams } from 'next/navigation' import React, { useEffect } from 'react' +import { PaymentSuccess } from './PaymentSuccess' +import { PaymentError } from './PaymentError' +import Link from 'next/link' export const PaymentDetail = () => { const searchParams = useSearchParams() const orderId = searchParams.get('order_id') + const status = searchParams.get('transaction_status') + const [getTransaction, { data, isLoading }] = useLazyGetTransactionQuery() useEffect(() => { if (orderId) { getTransaction({ order_id: orderId }) } }, [orderId]) + + if (status == 'settlement' || status == 'capture') + return ( + + ) + if (transactionErrorStatuses.includes(status ?? '')) + return return (
{isLoading ? ( @@ -21,11 +37,21 @@ export const PaymentDetail = () => {
) : ( data && ( -
-

Transaction Detail

-

{data.transaction_detail.package.name}

-

{data.transaction_detail.payment_type}

-

{data.transaction_detail.payment_merchant}

+
+

+ Transaction in Progress +

+

Transaction detail

+
+

Package Name

+

{data?.transaction_detail.package.name}

+

Payment Type

+

{data?.transaction_detail.payment_type?.toUpperCase()}

+

Merchant

+

{data?.transaction_detail.payment_merchant?.toUpperCase()}

+

Status

+

{data?.transaction_detail.status?.toUpperCase()}

+
+ + +
) )} diff --git a/src/app/payment/PaymentError.tsx b/src/app/payment/PaymentError.tsx new file mode 100644 index 0000000..50f2892 --- /dev/null +++ b/src/app/payment/PaymentError.tsx @@ -0,0 +1,27 @@ +'use client' +import { Button } from '@/components/elements/Button' +import Link from 'next/link' +export interface PaymentDetailI { + status: string +} +export const PaymentError = ({ status }: PaymentDetailI) => { + return ( +
+
+

+ Payment Failed +

+

+ Transaction status:{' '} + + {status.toUpperCase()} + +

+ + + + +
+
+ ) +} diff --git a/src/app/payment/PaymentSuccess.tsx b/src/app/payment/PaymentSuccess.tsx new file mode 100644 index 0000000..b900b1a --- /dev/null +++ b/src/app/payment/PaymentSuccess.tsx @@ -0,0 +1,32 @@ +'use client' + +import { Button } from '@/components/elements/Button' +import { useLazyGetTransactionQuery } from '@/redux/api/paymentApi' +import Link from 'next/link' +import { useSearchParams } from 'next/navigation' +import { useEffect } from 'react' +import { PaymentDetailI } from './PaymentError' + +interface PaymentSuccessI extends PaymentDetailI { + packageName: string +} +export const PaymentSuccess = ({ status, packageName }: PaymentSuccessI) => { + const searchParams = useSearchParams() + + return ( +
+
+

+ Payment Success +

+

+ Enjoy the {packageName} Package +

+ + + + +
+
+ ) +} diff --git a/src/app/profile/SubscriptionHistory.tsx b/src/app/profile/SubscriptionHistory.tsx index c1ded86..f9115d3 100644 --- a/src/app/profile/SubscriptionHistory.tsx +++ b/src/app/profile/SubscriptionHistory.tsx @@ -9,12 +9,9 @@ export const SubscriptionHistory: React.FC<{ return ( -

- Subscription History -

+

Subscription History

{data.map((history) => (

{ + const { data, isLoading } = useGetTransactionListQuery() + + return ( +
+

Transaction History

+ {isLoading ? ( +
+ ) : ( +
+ + + + + + + + + + + + {data && data?.length > 0 ? ( + data + ?.filter((transaction) => { + if (status) return transaction.status === status + return transaction + }) + .map( + ({ + checkout_time, + expiry_time, + package: { name }, + price, + id, + status, + midtrans_url, + }) => ( + + + + + + + + + ) + ) + ) : ( + + )} + +
+ Checkout Time + + Package Plan + + Price + + Expiry Time + + Status +
+ {dayjs(checkout_time).format('ddd, D MMM YY HH:mm')} + + {name} + + {formatRupiah(price)} + + {dayjs(expiry_time).format('ddd, D MMM YY HH:mm')} + + +
+ No transactions recorded +
+
+ )} +
+ ) +} diff --git a/src/app/profile/constant.ts b/src/app/profile/constant.ts index 3a2a49b..db2be98 100644 --- a/src/app/profile/constant.ts +++ b/src/app/profile/constant.ts @@ -1,6 +1,31 @@ +import { cva } from 'class-variance-authority' + const CHIP_STYLE = '!font-bold !p-5 !border-none' export const CHIP_STYLE_ACTIVE = CHIP_STYLE + ' ' + '!bg-teal-600 !text-teal-50' export const CHIP_STYLE_INACTIVE = CHIP_STYLE + ' ' + '!bg-teal-50 !text-teal-400' + +export const StatusVariants = cva('font-bold', { + variants: { + variant: { + success: 'text-emerald-500', + error: 'text-rose-500', + pending: 'text-gray-500', + }, + }, + defaultVariants: { + variant: 'pending', + }, +}) + +export const HoverRowVariants = cva('hover:scale-[1.001]', { + variants: { + hover: { + success: 'hover:bg-emerald-50', + error: 'hover:bg-rose-50', + pending: 'hover:bg-gray-50', + }, + }, +}) diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 4372bdd..1266ea3 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -21,12 +21,17 @@ import { useGetSubscriptionsQuery, } from '@/redux/api/subscriptionApi' import { CHIP_STYLE_ACTIVE, CHIP_STYLE_INACTIVE } from './constant' +import { TransactionHistory } from './TransactionHistory' +import { useSearchParams } from 'next/navigation' type ChipType = 'event' | 'history' export default function Profile() { + const searchParams = useSearchParams() const [openPopup, setOpenPopup] = useState(false) - const [chipType, setChipType] = React.useState('event') + const [chipType, setChipType] = React.useState( + (searchParams.get('tab') as ChipType) ?? 'event' + ) const { data, isLoading, isError } = useGetProfileQuery() const { data: events } = useGetEventsQuery() const dispatch = useDispatch() @@ -86,12 +91,15 @@ export default function Profile() { /> ))} - + ) : ( - renderSubscriptionHistory() +
+ {renderSubscriptionHistory()} + +
)}
diff --git a/src/components/elements/Button/index.tsx b/src/components/elements/Button/index.tsx index 068ae45..6580cfe 100644 --- a/src/components/elements/Button/index.tsx +++ b/src/components/elements/Button/index.tsx @@ -29,13 +29,18 @@ export const Button = ({