Skip to content

Commit

Permalink
feat: submit payment notice
Browse files Browse the repository at this point in the history
  • Loading branch information
mgramigna committed Dec 24, 2023
1 parent c2d7454 commit 034d1b8
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 1 deletion.
56 changes: 55 additions & 1 deletion apps/expo/src/app/(app)/profile/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { Skeleton } from '@/components/atoms/Skeleton';
import { Text } from '@/components/atoms/Text';
import { AddEditInsuranceModal } from '@/components/molecules/AddEditInsuranceModal';
import { ScreenView } from '@/components/molecules/ScreenView';
import {
SubmitPaymentModal,
type PaymentFormType,
} from '@/components/molecules/SubmitPaymentModal';
import { CoverageDetail } from '@/components/organisms/CoverageDetail';
import { DocumentDownloadCard } from '@/components/organisms/DocumentDownloadCard';
import { type InsuranceFormType } from '@/components/organisms/InsuranceForm';
Expand All @@ -13,12 +17,14 @@ import { palette } from '@/theme/colors';
import { api } from '@/utils/api';
import { getCoverageResource } from '@/utils/fhir';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';

import { type Coverage } from '@careforge/canvas';

const Billing = () => {
const { patientId } = useAuth();
const [insuranceModalOpen, setInsuranceModalOpen] = useState(false);
const [paymentModalOpen, setPaymentModalOpen] = useState(false);

const {
data: invoiceBundle,
Expand Down Expand Up @@ -57,6 +63,22 @@ const Billing = () => {
},
});

const createPaymentMutation = api.paymentnotice.create.useMutation({
onSuccess: async () => {
await utils.documentreference.search.invalidate({
category: 'invoicefull',
});
setPaymentModalOpen(false);
},
onError: (error) => {
if (error.data?.code === 'UNPROCESSABLE_CONTENT') {
Alert.alert('Submitted amount must be less than or equal to the amount owed');
} else {
Alert.alert('Something went wrong');
}
},
});

const handleCoverageSave = useCallback(
(form: InsuranceFormType) => {
const coverage = getCoverageResource({
Expand All @@ -69,6 +91,26 @@ const Billing = () => {
[createCoverageMutation, patientId],
);

const handlePaymentSubmit = useCallback(
(form: PaymentFormType) => {
createPaymentMutation.mutate({
resourceType: 'PaymentNotice',
status: 'active',
request: {
reference: `Patient/${patientId}`,
},
created: dayjs().toISOString(),
payment: {},
recipient: {},
amount: {
value: parseFloat(form.amount),
currency: 'USD',
},
});
},
[createPaymentMutation, patientId],
);

const onRefresh = useCallback(() => {
Promise.all([refetchInvoices(), refetchCoverage()]).catch(() =>
Alert.alert('Error refreshing data'),
Expand Down Expand Up @@ -115,6 +157,11 @@ const Billing = () => {
</View>
)}
</View>
{(invoiceBundle?.total ?? 0) > 0 && (
<View className="mt-8">
<Button text="Pay Bills" onPress={() => setPaymentModalOpen(true)} />
</View>
)}
<View className="mt-8">
<Text className="text-3xl" weight="bold">
My Insurance
Expand All @@ -131,15 +178,22 @@ const Billing = () => {
</Fragment>
))}
</View>
<View className="mt-8">
<View className="mt-8 pb-24">
<Button text="Add New Insurance" onPress={() => setInsuranceModalOpen(true)} />
</View>
</ScrollView>
</ScreenView>
<SubmitPaymentModal
isOpen={paymentModalOpen}
onClose={() => setPaymentModalOpen(false)}
onSubmit={handlePaymentSubmit}
isMutating={createPaymentMutation.isPending}
/>
<AddEditInsuranceModal
isOpen={insuranceModalOpen}
onClose={() => setInsuranceModalOpen(false)}
onSubmit={handleCoverageSave}
isMutating={createCoverageMutation.isPending}
/>
</>
) : null;
Expand Down
88 changes: 88 additions & 0 deletions apps/expo/src/components/molecules/SubmitPaymentModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Modal, ScrollView, View } from 'react-native';
import { zodResolver } from '@hookform/resolvers/zod';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';

import { Button } from '../atoms/Button';
import { Text } from '../atoms/Text';
import { TextInput } from '../atoms/TextInput';
import { InputLabel } from './InputLabel';
import { ScreenView } from './ScreenView';

const PaymentFormSchema = z.object({
amount: z.string().regex(/^\$?[0-9]+(\.[0-9][0-9])?$/, 'Please enter a valid dollar amount'),
});

export type PaymentFormType = z.infer<typeof PaymentFormSchema>;

export const SubmitPaymentModal = ({
isOpen,
onClose,
onSubmit,
isMutating,
}: {
isOpen: boolean;
onClose: () => void;
onSubmit: (form: PaymentFormType) => void;
isMutating?: boolean;
}) => {
const {
handleSubmit,
formState: { isValid, errors },
control,
} = useForm<PaymentFormType>({
resolver: zodResolver(PaymentFormSchema),
defaultValues: {
amount: '',
},
});

return (
<Modal
animationType="slide"
visible={isOpen}
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<ScreenView>
<ScrollView className="h-full">
<View>
<Text weight="bold" className="mb-4 text-3xl">
Submit Payment
</Text>
</View>
<View className="mt-12">
<InputLabel label="Amount (USD)" required />
<Controller
control={control}
name="amount"
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
keyboardType="decimal-pad"
value={value}
onChangeText={onChange}
onBlur={onBlur}
hasError={!!errors.amount}
/>
)}
/>
<Text className="text-red-200">{errors.amount?.message}</Text>
</View>
<View className="mt-8 flex flex-row gap-8">
<View className="flex-1">
<Button text="Close" variant="secondary" onPress={onClose} />
</View>
<View className="flex-1">
<Button
text="Submit Payment"
disabled={!isValid}
onPress={handleSubmit(onSubmit)}
isLoading={isMutating}
/>
</View>
</View>
</ScrollView>
</ScreenView>
</Modal>
);
};
1 change: 1 addition & 0 deletions packages/canvas/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function getCanvasErrorFromResponse(
.returnType<Err<never, CanvasError>>()
.with(400, () => err({ errorType: 'BAD_REQUEST' as const, details: responseText }))
.with(404, () => err({ errorType: 'NOT_FOUND' as const, details: responseText }))
.with(422, () => err({ errorType: 'PARSE' as const, details: responseText }))
.with(500, () => err({ errorType: 'INTERNAL' as const, details: responseText }))
.otherwise(() =>
err({
Expand Down

0 comments on commit 034d1b8

Please sign in to comment.