working auto

This commit is contained in:
2025-09-27 00:13:40 -04:00
parent 99eacbb51d
commit d6525f2d24
10 changed files with 787 additions and 73 deletions

View File

@@ -19,48 +19,94 @@
<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 -->
<!-- Top Row: Customer Info/Payment and Charge Breakdown/Pricing -->
<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 to Fill:</span>
<span>{{ calculateGallonsToFill() }} gallons</span>
<!-- Left Column: Customer Info and Payment Method -->
<div class="space-y-4">
<!-- Customer Info Card -->
<div class="bg-neutral rounded-lg p-5">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
</div>
<router-link v-if="customer && customer.id" :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
View Profile
</router-link>
</div>
<div class="flex justify-between">
<span>Price per Gallon:</span>
<span>${{ pricing.price_for_customer || currentOilPrice }}</span>
</div>
<div class="flex justify-between font-semibold">
<span>Subtotal:</span>
<span>${{ calculateSubtotal() }}</span>
</div>
<hr class="my-3">
<div class="flex justify-between font-bold text-lg">
<span>Estimated 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="font-mono text-sm">
<div> {{ selectedCard.card_number }}</div>
<div>{{ selectedCard.name_on_card }}</div>
<div>Expires: {{ selectedCard.expiration_month }}/{{ selectedCard.expiration_year }}</div>
<div>
<div>{{ customer.customer_address }}</div>
<div v-if="customer.customer_apt && customer.customer_apt !== 'None'">{{ customer.customer_apt }}</div>
<div>{{ customer.customer_town }}, {{ customerStateName }} {{ customer.customer_zip }}</div>
<div class="mt-2">{{ customer.customer_phone_number }}</div>
</div>
</div>
<div v-else class="text-gray-500 p-4">
No payment method selected
<!-- 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="font-mono text-sm">
<div> {{ selectedCard.card_number }}</div>
<div>{{ selectedCard.name_on_card }}</div>
<div>Expires: {{ selectedCard.expiration_month }}/{{ selectedCard.expiration_year }}</div>
</div>
</div>
<div v-else class="text-gray-500 p-4">
No payment method selected
</div>
</div>
</div>
<!-- Right Column: Charge Breakdown and Pricing -->
<div class="space-y-4">
<!-- 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 to Fill:</span>
<span>{{ calculateGallonsToFill() }} gallons</span>
</div>
<div class="flex justify-between">
<span>Price per Gallon:</span>
<span>${{ pricing.price_for_customer || currentOilPrice }}</span>
</div>
<div class="flex justify-between font-semibold">
<span>Subtotal:</span>
<span>${{ calculateSubtotal() }}</span>
</div>
<hr class="my-3">
<div class="flex justify-between font-bold text-lg">
<span>Estimated Total:</span>
<span>${{ calculateTotalAmount() }}</span>
</div>
</div> <!-- close space-y-2 -->
</div> <!-- close bg-base-100 -->
<!-- Pricing Chart Card -->
<div class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Today's Price Per Gallon</h3>
<div class="overflow-x-auto">
<table class="table table-sm w-full">
<thead>
<tr>
<th>Gallons</th>
<th>Total Price</th>
</tr>
</thead>
<tbody>
<tr v-for="tier in pricingTiers" :key="tier.gallons" :class="isPricingTierSelected(tier.gallons) ? 'bg-blue-600 text-white hover:bg-blue-600' : 'hover'">
<td>{{ tier.gallons }}</td>
<td>${{ tier.price.toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
@@ -92,6 +138,14 @@
:disabled="loading"
/>
</div>
<div class="flex flex-wrap gap-2 mt-2">
<button v-for="amount in quickGallonAmounts"
:key="amount"
@click.prevent="setGallons(amount)"
:class="['btn', 'btn-xs', selectedGallonAmount == amount ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">
{{ amount }} gal
</button>
</div>
</div>
<!-- Action Buttons -->
@@ -162,6 +216,7 @@ export default defineComponent({
deliveryId: this.$route.params.id as string,
loaded: false,
chargeAmount: 0,
quickGallonAmounts: [100, 125, 150, 175, 200, 220],
loading: false,
action: '', // 'preauthorize' or 'charge'
error: '',
@@ -222,17 +277,26 @@ export default defineComponent({
date: "",
},
currentOilPrice: 0,
pricingTiers: [] as { gallons: number; price: number }[],
}
},
computed: {
selectedCard(): any {
return this.credit_cards.find((card: any) => card.main_card) || this.credit_cards[0]
},
customerStateName(): string {
const states: Record<number, string> = { 0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire', 3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York' };
return states[this.customer.customer_state] || 'Unknown state';
},
selectedGallonAmount(): number | null {
return this.quickGallonAmounts.find(gal => this.calculatePriceForGallons(gal).toFixed(2) === this.chargeAmount.toFixed(2)) || null;
}
},
mounted() {
this.loadData(this.deliveryId)
this.getPricingTiers()
},
created() {
@@ -376,6 +440,55 @@ export default defineComponent({
})
},
getPricingTiers() {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers";
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader()
})
.then((response: any) => {
this.pricingTiers = Object.entries(response.data).map(([gallons, price]) => ({ gallons: parseInt(gallons, 10), price: Number(price) }));
})
.catch(() => {
notify({
title: "Pricing Error",
text: "Could not retrieve today's pricing.",
type: "error"
});
});
},
isPricingTierSelected(tierGallons: number | string): boolean {
const calculated = this.calculateGallonsToFill()
return calculated == Number(tierGallons)
},
calculatePriceForGallons(gallons: number): number {
let priceForGallons = 0;
const sortedTiers = [...this.pricingTiers].sort((a, b) => Number(a.gallons) - Number(b.gallons));
// Find the highest tier that is less than or equal to the gallons ordered
let applicableTier = sortedTiers.filter(t => gallons >= Number(t.gallons)).pop();
if (applicableTier) {
const pricePerGallon = Number(applicableTier.price) / Number(applicableTier.gallons);
priceForGallons = gallons * pricePerGallon;
} else if (sortedTiers.length > 0) {
// Fallback to the lowest tier's price/gallon if no tier is met (e.g., ordering 50 gallons when lowest tier is 100)
const lowestTier = sortedTiers[0];
const pricePerGallon = Number(lowestTier.price) / Number(lowestTier.gallons);
priceForGallons = gallons * pricePerGallon;
}
return priceForGallons;
},
setGallons(gallons: number) {
this.chargeAmount = this.calculatePriceForGallons(gallons);
},
calculateGallonsToFill() {
return this.autoDelivery.tank_size - this.autoDelivery.estimated_gallons_left
},
@@ -514,8 +627,8 @@ export default defineComponent({
// Create Tickets_Auto_Delivery after successful charge
const ticketPayload = {
gallons_delivered: this.calculateGallonsToFill(),
payment_type: 1, // 1 for charge, 11 for preauthorize
gallons_delivered: 0,
payment_type: 11, // 11 for Authorize charge
payment_card_id: this.selectedCard.id,
payment_status: response.data.status // 0 = APPROVED
};

View File

@@ -217,6 +217,24 @@
</div>
</div>
</div>
<!-- Payment Capture Success Modal -->
<div class="modal" :class="{ 'modal-open': showPaymentModal }">
<div class="modal-box">
<h3 class="font-bold text-lg">Payment Captured Successfully</h3>
<div class="py-4">
<div v-if="modalStep === 0" class="text-center">
<span class="text-lg mb-3">Transaction ID: {{ transaction?.auth_net_transaction_id }}</span>
<div class="loading loading-spinner loading-lg text-success mb-3"></div>
<p class="text-sm text-gray-600">Processing payment capture...</p>
</div>
<div v-else-if="modalStep === 1" class="text-center">
<span class="text-lg mb-3">Transaction ID: {{ transaction?.auth_net_transaction_id }}</span>
<p class="text-lg">Captured Amount: ${{ modalCapturedAmount }}</p>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
@@ -236,6 +254,9 @@ export default defineComponent({
captureAmount: 0,
preAuthAmount: 0,
transaction: null as any,
showPaymentModal: false,
modalStep: 0,
modalCapturedAmount: 0,
userCard: {
date_added: '',
@@ -252,10 +273,10 @@ export default defineComponent({
},
customerDescription: {
customer_id: 0,
account_number: '',
account_number: '',
company_id: 0,
fill_location: 0,
description: '',
description: '',
},
customer: {
id: 0,
@@ -324,7 +345,7 @@ export default defineComponent({
price_emergency: 0,
date: "",
},
total_amount: 0,
discount: 0,
total_amount_after_discount: 0,
@@ -356,7 +377,7 @@ export default defineComponent({
if (this.autoTicket.payment_card_id) {
this.getPaymentCard(this.autoTicket.payment_card_id)
}
})
.catch(() => {
notify({
@@ -366,7 +387,7 @@ export default defineComponent({
});
});
},
getAutoDelivery(delivery_id: any) {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/finddelivery/" + delivery_id;
axios({
@@ -379,7 +400,7 @@ export default defineComponent({
this.autoDelivery = response.data;
this.getCustomer(this.autoDelivery.customer_id)
this.getCustomerDescription(this.autoDelivery.customer_id)
})
.catch(() => {
notify({
@@ -490,14 +511,12 @@ export default defineComponent({
if (response.data && response.data.status === 0) {
this.autoTicket.payment_status = 3; // Update local status immediately
notify({
title: "Success",
text: "Payment captured successfully!",
type: "success",
});
this.modalCapturedAmount = this.captureAmount;
this.showPaymentModal = true;
// Close the ticket and unassign from delivery
this.closeTicket(this.autoTicket.id);
this.$router.push({ name: 'auto' });
setTimeout(() => { this.modalStep = 1 }, 2000);
setTimeout(() => { this.showPaymentModal = false; this.$router.push({ name: 'auto' }) }, 4000);
} else if (response.data && response.data.status === 1) {
const reason = response.data.rejection_reason || "The payment was declined by the gateway.";
notify({

View File

@@ -134,7 +134,42 @@
<div class="font-bold">When Delivered</div>
<div class="opacity-80">{{ deliveryOrder.when_delivered }}</div>
</div>
</div>
</div>
<!-- Transaction Summary -->
<div v-if="transaction && transaction.auth_net_transaction_id" class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Transaction Summary</h3>
<div class="space-y-3">
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<div class="flex justify-between">
<span class="font-bold">Transaction ID:</span>
<span class="font-mono">{{ transaction.auth_net_transaction_id }}</span>
</div>
<div class="flex justify-between">
<span class="font-bold">Pre-Auth Amount:</span>
<span>${{ transaction.preauthorize_amount || '0.00' }}</span>
</div>
<div class="flex justify-between">
<span class="font-bold">Charge Amount:</span>
<span>${{ transaction.charge_amount || '0.00' }}</span>
</div>
<div class="flex justify-between">
<span class="font-bold">Type:</span>
<span :class="getTypeColor(transaction.transaction_type)">{{ transaction.transaction_type === 0 ? 'Charge' : transaction.transaction_type === 1 ? 'Auth' : 'Capture' }}</span>
</div>
<div class="flex justify-between">
<span class="font-bold">Date:</span>
<span>{{ transaction.created_at }}</span>
</div>
<div class="flex justify-between">
<span class="font-bold">Status:</span>
<span :class="transaction.status === 0 ? 'text-success' : 'text-error'">
{{ transaction.status === 0 ? 'Approved' : 'Declined' }}
</span>
</div>
</div>
</div>
</div>
@@ -173,6 +208,9 @@
<!-- Capture Payment Form -->
<div class="bg-base-100 rounded-lg p-5">
<h2 class="text-2xl font-bold mb-4">Capture Payment</h2>
<div v-if="preAuthAmount > 0" class="mb-2 text-sm text-orange-600">
Max Capture amount: ${{ preAuthAmount.toFixed(2) }}
</div>
<div class="space-y-4">
<div class="form-control">
<label class="label">
@@ -219,6 +257,24 @@
</div>
</div>
</div>
<!-- Payment Capture Success Modal -->
<div class="modal" :class="{ 'modal-open': showPaymentModal }">
<div class="modal-box">
<h3 class="font-bold text-lg">Payment Captured Successfully</h3>
<div class="py-4">
<div v-if="modalStep === 0" class="text-center">
<span class="text-lg mb-3">Transaction ID: {{ transaction?.auth_net_transaction_id }}</span>
<div class="loading loading-spinner loading-lg text-success mb-3"></div>
<p class="text-sm text-gray-600">Processing payment capture...</p>
</div>
<div v-else-if="modalStep === 1" class="text-center">
<span class="text-lg mb-3">Transaction ID: {{ transaction?.auth_net_transaction_id }}</span>
<p class="text-lg">Captured Amount: ${{ modalCapturedAmount }}</p>
</div>
</div>
</div>
</div>
<Footer />
</template>
@@ -248,6 +304,9 @@ export default defineComponent({
captureAmount: 0,
preAuthAmount: 0,
transaction: null as any,
showPaymentModal: false,
modalStep: 0,
modalCapturedAmount: 0,
userCard: {
date_added: '',
@@ -493,13 +552,11 @@ export default defineComponent({
// ✅ FIX: Improved logic to handle both success and declines properly.
if (response.data && response.data.status === 0) {
// This is the APPROVED case
notify({
title: "Success",
text: "Payment captured successfully!",
type: "success",
});
this.$router.push({ name: 'deliveryOrder', params: { id: this.$route.params.id } });
this.modalCapturedAmount = this.captureAmount;
this.showPaymentModal = true;
setTimeout(() => { this.modalStep = 1 }, 2000);
setTimeout(() => { this.showPaymentModal = false; this.$router.push({ name: 'deliveryOrder', params: { id: this.$route.params.id } }) }, 4000);
} else if (response.data && response.data.status === 1) {
// This is the DECLINED case
const reason = response.data.rejection_reason || "The payment was declined by the gateway.";
@@ -606,6 +663,16 @@ export default defineComponent({
cancelCapture() {
this.$router.push({ name: 'finalizeTicket', params: { id: this.$route.params.id } });
},
getTypeColor(transactionType: number) {
switch (transactionType) {
case 1: return 'text-blue-600'; // Auth
case 0: return 'text-orange-600'; // Charge
case 2: return 'text-purple-600'; // Capture
case 3: return 'text-green-600'; // Delivery/Other
default: return 'text-gray-600';
}
},
},
})
</script>

View File

@@ -183,6 +183,24 @@
</div>
</div>
</div>
<!-- Payment Capture Success Modal -->
<div class="modal" :class="{ 'modal-open': showPaymentModal }">
<div class="modal-box">
<h3 class="font-bold text-lg">Payment Captured Successfully</h3>
<div class="py-4">
<div v-if="modalStep === 0" class="text-center">
<span class="text-lg mb-3">Transaction ID: {{ transaction?.auth_net_transaction_id }}</span>
<div class="loading loading-spinner loading-lg text-success mb-3"></div>
<p class="text-sm text-gray-600">Processing payment capture...</p>
</div>
<div v-else-if="modalStep === 1" class="text-center">
<span class="text-lg mb-3">Transaction ID: {{ transaction?.auth_net_transaction_id }}</span>
<p class="text-lg">Captured Amount: ${{ modalCapturedAmount }}</p>
</div>
</div>
</div>
</div>
<Footer />
</template>
@@ -248,6 +266,9 @@ const chargeAmount = ref<number>(0);
const transaction = ref(null as any);
const preAuthAmount = ref<number>(0);
const serviceTransactions = ref<ServiceTransaction[]>([]);
const showPaymentModal = ref(false);
const modalStep = ref(0);
const modalCapturedAmount = ref(0);
// --- Computed Properties for Cleaner Template ---
const stateName = computed(() => {
@@ -399,8 +420,10 @@ const chargeService = async () => {
{ withCredentials: true, headers: authHeader() }
);
notify({ title: "Success", text: "Service charged successfully!", type: "success" });
router.push({ name: 'customerProfile', params: { id: service.value!.customer_id } });
modalCapturedAmount.value = chargeAmount.value;
showPaymentModal.value = true;
setTimeout(() => { modalStep.value = 1 }, 2000);
setTimeout(() => { showPaymentModal.value = false; router.push({ name: 'customerProfile', params: { id: service.value!.customer_id } }) }, 4000);
return;
} else {
const reason = response.data?.rejection_reason || "The charge was declined.";
@@ -435,8 +458,10 @@ const chargeService = async () => {
{ withCredentials: true, headers: authHeader() }
);
notify({ title: "Success", text: "Payment captured successfully!", type: "success" });
router.push({ name: 'customerProfile', params: { id: service.value!.customer_id } });
modalCapturedAmount.value = chargeAmount.value;
showPaymentModal.value = true;
setTimeout(() => { modalStep.value = 1 }, 2000);
setTimeout(() => { showPaymentModal.value = false; router.push({ name: 'customerProfile', params: { id: service.value!.customer_id } }) }, 4000);
} else if (captureResponse.data?.status === 1) {
const reason = captureResponse.data.rejection_reason || "The payment was declined by the gateway.";
notify({ title: "Payment Declined", text: reason, type: "warn" });