Files
eamco_office_frontend/src/pages/pay/oil/authorize_preauthcharge.vue
2026-01-28 21:55:14 -05:00

675 lines
21 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- src/pages/pay/oil/authorize_preauthcharge.vue -->
<template>
<div class="flex">
<!-- Main Content -->
<div class="flex-1 px-8 py-6">
<!-- Breadcrumbs & Header -->
<div class="text-sm breadcrumbs mb-6">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'payOil', params: { id: deliveryId } }">Payment Confirmation</router-link></li>
<li>Payment Authorization</li>
</ul>
</div>
<!-- 2x2 Grid Layout -->
<div class="max-w-6xl">
<h1 class="text-3xl font-bold mb-8">Payment Authorization Authorize.net</h1>
<!-- Top Row: Charge Breakdown and Payment Method -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Charge Breakdown -->
<div class="bg-base-100 rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Charge Breakdown</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>Gallons Ordered:</span>
<span>{{ delivery.gallons_ordered || 0 }} gallons</span>
</div>
<div class="flex justify-between">
<span>Price per Gallon:</span>
<span>${{ delivery.customer_price || 0 }}</span>
</div>
<div class="flex justify-between font-semibold">
<span>Subtotal:</span>
<span>${{ calculateSubtotal() }}</span>
</div>
<div v-if="delivery.prime == 1" class="flex justify-between text-sm">
<span>Prime Fee:</span>
<span>+ ${{ pricing.price_prime || 0 }}</span>
</div>
<div v-if="delivery.same_day == 1" class="flex justify-between text-sm">
<span>Same Day Fee:</span>
<span>+ ${{ pricing.price_same_day || 0 }}</span>
</div>
<div v-if="delivery.emergency == 1" class="flex justify-between text-sm">
<span>Emergency Fee:</span>
<span>+ ${{ pricing.price_emergency || 0 }}</span>
</div>
<div v-if="promo_active" class="flex justify-between text-success">
<span>{{ promo.name_of_promotion }}:</span>
<span>- ${{ discount }}</span>
</div>
<hr class="my-3">
<div class="flex justify-between font-bold text-lg">
<span>Total:</span>
<span>${{ calculateTotalAmount() }}</span>
</div>
</div> <!-- close space-y-2 -->
</div> <!-- close bg-base-100 -->
<!-- Credit Card Display -->
<div class="bg-base-100 rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Payment Method</h3>
<div v-if="selectedCard" class="bg-base-200 p-4 rounded-md">
<div class="flex justify-between items-center mb-2">
<div class="font-semibold">{{ selectedCard.type_of_card }}</div>
<div v-if="selectedCard.main_card" class="badge badge-primary">Primary</div>
</div>
<div class="mt-3 text-sm font-mono tracking-wider">
<p>{{ selectedCard.card_number }}</p>
<p>{{ selectedCard.name_on_card }}</p>
<p>
Exp:
<span v-if="Number(selectedCard.expiration_month) < 10">0</span>{{ selectedCard.expiration_month }} / {{ selectedCard.expiration_year }}
</p>
<p>CVV: {{ selectedCard.security_number }}</p>
</div>
<div class="divider my-2"></div>
<div class="flex justify-end gap-2">
<router-link :to="{ name: 'cardedit', params: { id: selectedCard.id }}" class="link link-hover text-xs">Edit</router-link>
</div>
</div>
<div v-else class="text-gray-500 p-4">
No payment method selected
</div>
</div>
</div>
<!-- Error/Success Messages -->
<div class="mb-6">
<div v-if="error" class="alert alert-error">
<span>{{ error }}</span>
</div>
<div v-if="success" class="alert alert-success">
<span>{{ success }}</span>
</div>
</div>
<!-- Bottom Section: Charge Amount Input and Action Buttons -->
<div class="bg-base-100 rounded-lg p-6">
<div class="flex flex-col lg:flex-row lg:items-center gap-6">
<!-- Charge Amount Input - Compact -->
<div class="flex-1">
<h3 class="text-lg font-semibold mb-2">Charge Amount</h3>
<div class="flex">
<span class="inline-flex items-center px-3 text-sm bg-base-200 rounded-l-lg border border-r-0">$</span>
<input
v-model="chargeAmount"
type="number"
step="0.01"
class="input input-bordered flex-1 rounded-l-none"
placeholder="Enter amount"
:disabled="loading"
/>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3 align-bottom">
<router-link :to="{ name: 'payOil', params: { id: deliveryId } }">
<button class="btn btn-ghost">Cancel</button>
</router-link>
<button
@click="handlePreauthorize"
class="btn btn-success"
:disabled="loading || !chargeAmount"
>
<span v-if="loading && action === 'preauthorize'" class="loading loading-spinner loading-sm"></span>
Preauthorize
</button>
<button
@click="handleChargeNow"
class="btn btn-warning text-black"
:disabled="loading || !chargeAmount"
>
<span v-if="loading && action === 'charge'" class="loading loading-spinner loading-sm"></span>
Charge Now
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Charge Confirmation Modal -->
<div class="modal" :class="{ 'modal-open': isChargeConfirmationModalVisible }">
<div class="modal-box">
<h3 class="font-bold text-lg text-warning"> Warning: Charge Now</h3>
<p class="py-4">
You are about to <strong>immediately charge</strong> this customer's card
for <strong>${{ chargeAmount.toFixed(2) }}</strong>.
<br><br>
This action is <strong>not reversible</strong> and will debit the customer's account immediately.
<br><br>
Are you sure you want to proceed with the charge?
</p>
<div class="modal-action">
<button @click="proceedWithCharge" class="btn btn-warning">
Yes, Charge Now
</button>
<button @click="cancelCharge" class="btn btn-ghost">
Cancel
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import axios, { AxiosResponse, AxiosError } from 'axios'
import authHeader from '../../../services/auth.header'
import { notify } from "@kyvg/vue3-notification"
import type {
DeliveryFormData,
CustomerFormData,
CreditCardFormData,
PricingData,
PromoData,
DeliveryOrderResponse,
DeliveryTotalResponse,
OilPricingResponse,
PromoResponse,
WhoAmIResponse,
UpdateStatusResponse
} from '../../../types/models'
// Router and route
const route = useRoute()
const router = useRouter()
// Reactive data
const deliveryId = ref(route.params.id as string)
const loaded = ref(false)
const chargeAmount = ref(0)
const loading = ref(false)
const action = ref('') // 'preauthorize' or 'charge'
const error = ref('')
const success = ref('')
const isChargeConfirmationModalVisible = ref(false)
const user = ref({
user_id: 0,
})
const delivery = ref<DeliveryFormData>({
id: 0,
customer_id: 0,
customer_name: '',
customer_address: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
gallons_ordered: 0,
customer_asked_for_fill: 0,
gallons_delivered: 0,
customer_filled: 0,
delivery_status: 0,
when_ordered: '',
when_delivered: '',
expected_delivery_date: '',
automatic: 0,
oil_id: 0,
supplier_price: 0,
customer_price: 0,
customer_temperature: 0,
dispatcher_notes: '',
prime: 0,
promo_id: 0,
emergency: 0,
same_day: 0,
payment_type: 0,
payment_card_id: 0,
driver_employee_id: 0,
driver_first_name: '',
driver_last_name: '',
pre_charge_amount: 0,
total_price: 0,
service_id: null,
})
const credit_cards = ref<CreditCardFormData[]>([
{
id: 0,
name_on_card: '',
main_card: false,
card_number: '',
expiration_month: '',
type_of_card: '',
last_four_digits: '',
expiration_year: '',
security_number: '',
}
])
const customer = ref<CustomerFormData>({
id: 0,
user_id: 0,
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_address: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
account_number: '',
})
const pricing = ref<PricingData>({
price_from_supplier: 0,
price_for_customer: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
date: "",
})
const promo_active = ref(false)
const promo = ref<PromoData>({
name_of_promotion: '',
description: '',
money_off_delivery: 0,
text_on_ticket: ''
})
const total_amount = ref(0)
const discount = ref(0)
const total_amount_after_discount = ref(0)
// Computed properties
const selectedCard = computed(() => {
return credit_cards.value.find((card) => card.id === delivery.value.payment_card_id)
})
// Lifecycle
onMounted(() => {
loadData(deliveryId.value)
})
// Watchers
watch(() => route.params.id, (newId) => {
if (newId !== deliveryId.value) {
resetState()
deliveryId.value = newId as string
loadData(newId as string)
}
})
// Functions
const resetState = () => {
loading.value = false
action.value = ''
error.value = ''
success.value = ''
chargeAmount.value = 0
promo_active.value = false
total_amount.value = 0
discount.value = 0
total_amount_after_discount.value = 0
deliveryId.value = route.params.id as string
}
const loadData = (deliveryId: string) => {
userStatus()
getOilOrder(deliveryId)
sumdelivery(deliveryId)
getOilPricing()
updatestatus()
}
const updatestatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/updatestatus';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<UpdateStatusResponse>) => {
if (response.data.update)
console.log("Updated Status of Deliveries")
})
}
const updateChargeAmount = () => {
// Only update if we have all necessary data
if (total_amount_after_discount.value > 0 &&
pricing.value.price_prime !== undefined &&
pricing.value.price_same_day !== undefined &&
pricing.value.price_emergency !== undefined) {
chargeAmount.value = calculateTotalAsNumber();
return true;
}
return false;
}
const sumdelivery = (delivery_id: number | string) => {
let path = import.meta.env.VITE_BASE_URL + "/delivery/total/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<DeliveryTotalResponse>) => {
if (response.data.ok) {
total_amount.value = parseFloat(String(response.data.total_amount)) || 0;
discount.value = parseFloat(String(response.data.discount)) || 0;
total_amount_after_discount.value = parseFloat(String(response.data.total_amount_after_discount)) || 0;
// Try to update charge amount with complete pricing
const updated = updateChargeAmount();
// Fallback only if pricing not loaded yet and calculation didn't run
if (!updated) {
if (promo_active.value) {
chargeAmount.value = total_amount_after_discount.value;
} else {
chargeAmount.value = total_amount.value;
}
}
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
}
const getPromo = (promo_id: number) => {
let path = import.meta.env.VITE_BASE_URL + "/promo/" + promo_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: AxiosResponse<PromoResponse>) => {
if (response.data) {
promo.value = response.data
promo_active.value = true
// Trigger a charge amount update if all data is available
updateChargeAmount();
}
})
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: AxiosResponse<WhoAmIResponse>) => {
if (response.data.ok) {
user.value = response.data.user;
}
})
}
const getOilPricing = () => {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/table";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<OilPricingResponse>) => {
pricing.value = response.data;
// Try to update charge amount when pricing is loaded
updateChargeAmount();
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
}
const getOilOrder = (delivery_id: number | string) => {
let path = import.meta.env.VITE_BASE_URL + "/delivery/order/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<DeliveryOrderResponse>) => {
if (response.data && response.data.ok) {
delivery.value = response.data.delivery as DeliveryFormData;
getCustomer(delivery.value.customer_id)
getCreditCards(delivery.value.customer_id)
if (delivery.value.promo_id != null) {
getPromo(delivery.value.promo_id);
promo_active.value = true;
}
} else {
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
}
})
.catch((error: Error) => {
console.error("API Error in getOilOrder:", error);
notify({
title: "Error",
text: "Could not get delivery",
type: "error",
});
});
}
const getCreditCards = (user_id: number) => {
let path = import.meta.env.VITE_BASE_URL + '/payment/cards/' + user_id;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<CreditCardFormData[]>) => {
credit_cards.value = response.data
})
}
const getCustomer = (userid: number) => {
let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<CustomerFormData>) => {
customer.value = response.data
})
}
const calculateSubtotal = () => {
const gallons = delivery.value.gallons_ordered || 0
const pricePerGallon = delivery.value.customer_price || 0
return (gallons * pricePerGallon).toFixed(2)
}
const calculateTotalAmount = () => {
if (total_amount_after_discount.value == null || total_amount_after_discount.value === undefined) {
return '0.00';
}
let totalNum = Number(total_amount_after_discount.value);
if (isNaN(totalNum)) {
return '0.00';
}
if (delivery.value && delivery.value.prime == 1 && pricing.value && pricing.value.price_prime) {
totalNum += Number(pricing.value.price_prime) || 0;
}
if (delivery.value && delivery.value.same_day == 1 && pricing.value && pricing.value.price_same_day) {
totalNum += Number(pricing.value.price_same_day) || 0;
}
if (delivery.value && delivery.value.emergency == 1 && pricing.value && pricing.value.price_emergency) {
totalNum += Number(pricing.value.price_emergency) || 0;
}
return totalNum.toFixed(2);
}
const calculateTotalAsNumber = () => {
if (total_amount_after_discount.value == null || total_amount_after_discount.value === undefined) {
return 0;
}
let totalNum = Number(total_amount_after_discount.value);
if (isNaN(totalNum)) {
return 0;
}
if (delivery.value && delivery.value.prime == 1 && pricing.value && pricing.value.price_prime) {
totalNum += Number(pricing.value.price_prime) || 0;
}
if (delivery.value && delivery.value.same_day == 1 && pricing.value && pricing.value.price_same_day) {
totalNum += Number(pricing.value.price_same_day) || 0;
}
if (delivery.value && delivery.value.emergency == 1 && pricing.value && pricing.value.price_emergency) {
totalNum += Number(pricing.value.price_emergency) || 0;
}
return totalNum;
}
const handlePreauthorize = async () => {
await processPayment('preauthorize')
}
const handleChargeNow = async () => {
if (!selectedCard.value) {
error.value = 'No credit card found for this customer'
return
}
if (!chargeAmount.value || chargeAmount.value <= 0) {
error.value = 'Please enter a valid charge amount'
return
}
isChargeConfirmationModalVisible.value = true
}
const proceedWithCharge = async () => {
isChargeConfirmationModalVisible.value = false
await processPayment('charge')
}
const cancelCharge = () => {
isChargeConfirmationModalVisible.value = false
}
const processPayment = async (actionType: string) => {
if (!selectedCard.value) {
error.value = 'No credit card found for this customer'
return
}
loading.value = true
action.value = actionType
error.value = ''
success.value = ''
try {
// Step 2: If payment method is credit, perform the pre-authorization
if (actionType === 'preauthorize') {
if (!chargeAmount.value || chargeAmount.value <= 0) {
throw new Error("Pre-authorization amount must be greater than zero.");
}
const authPayload = {
card_id: selectedCard.value!.id,
preauthorize_amount: chargeAmount.value.toFixed(2),
delivery_id: delivery.value.id,
};
const authPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/authorize/saved-card/${customer.value.id}`;
const response = await axios.post(authPath, authPayload, { withCredentials: true, headers: authHeader() });
// Update payment type to 11 after successful preauthorization
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/authorize/${delivery.value.id}`, {}, { headers: authHeader() });
} catch (updateError) {
console.error('Failed to update payment type after preauthorization:', updateError);
}
// On successful authorization, show success and redirect
success.value = `Preauthorization successful! Transaction ID: ${response.data?.auth_net_transaction_id || 'N/A'}`;
setTimeout(() => {
router.push({ name: "customerProfile", params: { id: customer.value.id } });
}, 2000);
} else { // Handle 'charge' action
if (!chargeAmount.value || chargeAmount.value <= 0) {
throw new Error("Charge amount must be greater than zero.");
}
// Create a payload that matches the backend's TransactionCreateByCardID schema
const chargePayload = {
card_id: selectedCard.value!.id,
charge_amount: chargeAmount.value.toFixed(2),
delivery_id: delivery.value.id,
service_id: delivery.value.service_id || null,
// You can add other fields here if your schema requires them
};
// Use the correct endpoint for charging a saved card
const chargePath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/charge/saved-card/${customer.value.id}`;
console.log('=== DEBUG: Charge payload ===');
console.log('Calling endpoint:', chargePath);
console.log('Final payload being sent:', chargePayload);
const response = await axios.post(chargePath, chargePayload, { withCredentials: true, headers: authHeader() });
// Update payment type to 11 after successful charge
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/authorize/${delivery.value.id}`, {}, { headers: authHeader() });
} catch (updateError) {
console.error('Failed to update payment type after charge:', updateError);
}
// Status codes: 0 = APPROVED, 1 = DECLINED (based on backend TransactionStatus enum)
if (response.data && response.data.status === 0) { // 0 = APPROVED
success.value = `Charge successful! Transaction ID: ${response.data.auth_net_transaction_id || 'N/A'}`;
setTimeout(() => {
router.push({ name: "customerProfile", params: { id: customer.value.id } });
}, 2000);
} else {
// The error message from your backend will be more specific now
throw new Error(`Payment charge failed: ${response.data?.rejection_reason || 'Unknown error'}`);
}
}
} catch (err: unknown) {
const axiosErr = err as AxiosError<{ detail?: string }>;
console.log(err)
error.value = axiosErr.response?.data?.detail || `Failed to ${actionType} payment`
notify({
title: "Error",
text: error.value,
type: "error",
})
} finally {
loading.value = false
action.value = ''
}
}
</script>
<style scoped></style>