mirror of
https://github.com/hanzo-io/press.git
synced 2026-11-02 17:27:37 +00:00
241 lines
5.7 KiB
Vue
241 lines
5.7 KiB
Vue
<template>
|
|
<div>
|
|
<label class="block mt-4">
|
|
<span class="text-sm leading-4 text-gray-700"> </span>
|
|
<ErrorMessage class="mt-1" :message="paymentErrorMessage" />
|
|
</label>
|
|
|
|
<ErrorMessage class="mt-2" :message="errorMessage" />
|
|
|
|
<FormControl
|
|
type="autocomplete"
|
|
:options="teams"
|
|
size="sm"
|
|
variant="subtle"
|
|
placeholder="Payment Partner"
|
|
:disabled="false"
|
|
label="Select Payment Partner"
|
|
v-model="partnerInput"
|
|
class="mb-5"
|
|
/>
|
|
|
|
<div class="flex gap-5 col-2">
|
|
<FormControl
|
|
label="M-Pesa Phone Number"
|
|
v-model="this.phoneNumberInput"
|
|
name="phone_number"
|
|
autocomplete="off"
|
|
class="mb-5"
|
|
type="number"
|
|
placeholder="e.g 123456789"
|
|
/>
|
|
|
|
<FormControl
|
|
label="Tax ID"
|
|
v-model="this.taxIdInput"
|
|
name="tax_id"
|
|
autocomplete="off"
|
|
class="mb-5"
|
|
type="string"
|
|
placeholder="e.g A123456789A"
|
|
/>
|
|
</div>
|
|
<!-- Show amount after tax -->
|
|
<div v-if="showTaxInfo" class="flex col-2 gap-5">
|
|
<FormControl
|
|
label="Tax(%)"
|
|
disabled
|
|
:modelValue="taxPercentage"
|
|
type="number"
|
|
/>
|
|
|
|
<FormControl
|
|
label="Total Amount With Tax"
|
|
disabled
|
|
:modelValue="amountWithTax.toFixed(2)"
|
|
type="number"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mt-4 flex w-full justify-end">
|
|
<Button variant="solid" @click="onPayClick" :loading="paymentInProgress">
|
|
Make payment via M-Pesa
|
|
</Button>
|
|
</div>
|
|
|
|
<ErrorMessage v-if="errorMessage" :message="errorMessage" />
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { toast } from 'vue-sonner';
|
|
import { DashboardError } from '../../../utils/error';
|
|
import { frappeRequest, ErrorMessage } from 'frappe-ui';
|
|
export default {
|
|
name: 'BuyPrepaidCreditsMpesa',
|
|
props: {
|
|
amount: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
amountKES: {
|
|
type: Number,
|
|
required: true,
|
|
default: 1,
|
|
},
|
|
minimumAmount: {
|
|
type: Number,
|
|
required: true,
|
|
default: 10,
|
|
},
|
|
exchangeRate: {
|
|
type: Number,
|
|
required: true,
|
|
default: 129,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
paymentErrorMessage: null,
|
|
errorMessage: null,
|
|
paymentInProgress: false,
|
|
partnerInput: '',
|
|
phoneNumberInput: this.$team.doc.mpesa_phone_number || '',
|
|
taxIdInput: this.$team.doc.mpesa_tax_id || '',
|
|
teams: [],
|
|
taxPercentage: 1,
|
|
amountWithTax: 0,
|
|
showTaxInfo: false,
|
|
maximumAmount: 150000,
|
|
};
|
|
},
|
|
resources: {
|
|
requestForPayment() {
|
|
return {
|
|
url: 'press.api.billing.request_for_payment',
|
|
params: {
|
|
request_amount: this.amountKES,
|
|
sender: this.phoneNumberInput,
|
|
partner: this.partnerInput.value,
|
|
tax_id: this.taxIdInput,
|
|
amount_with_tax: this.amountWithTax,
|
|
phone_number: this.phoneNumberInput,
|
|
amount_usd: this.amount,
|
|
exchange_rate: this.exchangeRate,
|
|
},
|
|
validate() {
|
|
const pattern = /^[A-Z]\d{9}[A-Z]$/;
|
|
if (this.amount < this.minimumAmount) {
|
|
throw new DashboardError(
|
|
`Amount is less than the minimum allowed: ${this.minimumAmount}`,
|
|
);
|
|
}
|
|
if (this.amount > this.maximumAmount) {
|
|
throw new DashboardError(
|
|
`Amount is more than the maximum allowed: ${this.maximumAmount}`,
|
|
);
|
|
}
|
|
if (!this.partnerInput.value || !this.phoneNumberInput) {
|
|
throw new DashboardError(
|
|
'Both partner and phone number are required for payment.',
|
|
);
|
|
}
|
|
if (!this.taxIdInput) {
|
|
throw new DashboardError('Tax ID is required for payment.');
|
|
}
|
|
if (this.taxIdInput && !pattern.test(this.taxIdInput)) {
|
|
throw new DashboardError('Invalid Tax Id');
|
|
}
|
|
},
|
|
async onSuccess(data) {
|
|
if (data?.ResponseCode === '0') {
|
|
toast.success(
|
|
data.ResponseDescription ||
|
|
'Please follow the instructions on your phone',
|
|
);
|
|
} else {
|
|
toast.error(
|
|
data.ResponseDescription ||
|
|
'Something went wrong. Please try again.',
|
|
);
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
methods: {
|
|
async onPayClick() {
|
|
this.paymentInProgress = true;
|
|
try {
|
|
const response = await this.$resources.requestForPayment.submit();
|
|
if (response.ResponseCode === '0') {
|
|
toast.success(
|
|
response.Success ||
|
|
'Payment Initiated successfully, check your phone for instructions',
|
|
);
|
|
this.$emit('success');
|
|
} else {
|
|
throw new Error(response.ResponseDescription || 'Payment failed');
|
|
}
|
|
} catch (error) {
|
|
this.paymentErrorMessage =
|
|
error.message || 'Payment failed. Please try again.';
|
|
toast.error(this.paymentErrorMessage);
|
|
} finally {
|
|
this.paymentInProgress = false;
|
|
}
|
|
},
|
|
async fetchTeams() {
|
|
try {
|
|
const response = await frappeRequest({
|
|
url: '/api/method/press.api.regional_payments.mpesa.utils.display_mpesa_payment_partners',
|
|
method: 'GET',
|
|
});
|
|
if (Array.isArray(response)) {
|
|
this.teams = response;
|
|
} else {
|
|
console.log('No Data');
|
|
}
|
|
} catch (error) {
|
|
this.errorMessage = `Failed to fetch teams ${error.message}`;
|
|
}
|
|
},
|
|
totalAmountWithTax() {
|
|
const amountWithTax =
|
|
this.amountKES + (this.amountKES * this.taxPercentage) / 100;
|
|
this.amountWithTax = Math.round(amountWithTax);
|
|
},
|
|
async fetchTaxPercentage() {
|
|
try {
|
|
const taxPercentage = await frappeRequest({
|
|
url: '/api/method/press.api.regional_payments.mpesa.utils.get_tax_percentage',
|
|
method: 'GET',
|
|
params: {
|
|
payment_partner: this.partnerInput.value,
|
|
},
|
|
});
|
|
this.taxPercentage = taxPercentage;
|
|
} catch (error) {
|
|
this.errorMessage = `Failed to fetch tax percentage ${error.message}`;
|
|
}
|
|
},
|
|
},
|
|
watch: {
|
|
partnerInput() {
|
|
this.fetchTaxPercentage();
|
|
this.totalAmountWithTax();
|
|
this.showTaxInfo = true;
|
|
},
|
|
amountKES() {
|
|
this.totalAmountWithTax();
|
|
},
|
|
taxPercentage() {
|
|
this.totalAmountWithTax();
|
|
},
|
|
},
|
|
mounted() {
|
|
this.fetchTeams();
|
|
},
|
|
};
|
|
</script>
|