Răsfoiți Sursa

feat: add confirm withdraw

Gustavo Mantovani 2 săptămâni în urmă
părinte
comite
1b25cd9e43

+ 7 - 2
src/api/providerWithdrawal.js

@@ -5,12 +5,17 @@ export const getBalance = async () => {
   return data.payload
 }
 
+export const getWithdrawalFees = async () => {
+  const { data } = await api.get('/provider/withdrawals/fees')
+  return data.payload
+}
+
 export const getWithdrawals = async () => {
   const { data } = await api.get('/provider/withdrawals')
   return data.payload
 }
 
-export const requestWithdrawal = async () => {
-  const { data } = await api.post('/provider/withdrawals')
+export const requestWithdrawal = async (payload = {}) => {
+  const { data } = await api.post('/provider/withdrawals', payload)
   return data.payload
 }

+ 4 - 0
src/api/user.js

@@ -41,6 +41,10 @@ export const validateCode = async (email, phone, code, isLogin = false) => {
 };
 
 const removeEmptyRegistrationFields = (data) => {
+  if (typeof FormData !== "undefined" && data instanceof FormData) {
+    return data;
+  }
+
   if (Array.isArray(data)) {
     return data.map(removeEmptyRegistrationFields);
   }

+ 35 - 4
src/components/payments/WithdrawConfirmDialog.vue

@@ -11,10 +11,27 @@
           {{ $t('provider.payments.withdraw_confirm_title') }}
         </div>
         <div class="withdraw-amount text-secondary text-center">
-          {{ formatCurrency(amount) }}
+          {{ formatCurrency(netAmount) }}
         </div>
-        <div class="text-caption text-grey-6 text-center q-mt-xs">
-          {{ $t('provider.payments.withdraw_confirm_message', { amount: formatCurrency(amount) }) }}
+        <div class="withdraw-summary q-mt-md full-width">
+          <div class="row items-center justify-between q-mb-xs">
+            <span class="text-caption text-grey-6">{{ $t('provider.payments.withdraw_gross_amount_label') }}</span>
+            <span class="text-caption text-weight-bold text-text">{{ formatCurrency(grossAmount) }}</span>
+          </div>
+          <div class="row items-center justify-between q-mb-xs">
+            <span class="text-caption text-grey-6">{{ $t('provider.payments.withdraw_fee_label') }}</span>
+            <span class="text-caption text-weight-bold text-negative">
+              {{ formatCurrency(-gatewayFeeAmount) }}
+            </span>
+          </div>
+          <q-separator class="q-my-sm" />
+          <div class="row items-center justify-between">
+            <span class="text-caption text-grey-7 text-weight-bold">{{ $t('provider.payments.withdraw_net_amount_label') }}</span>
+            <span class="text-body2 text-weight-bold text-secondary">{{ formatCurrency(netAmount) }}</span>
+          </div>
+        </div>
+        <div class="text-caption text-grey-6 text-center q-mt-sm">
+          {{ $t('provider.payments.withdraw_confirm_message') }}
         </div>
       </q-card-section>
 
@@ -48,7 +65,15 @@ import { useDialogPluginComponent } from 'quasar';
 import { formatCurrency } from 'src/helpers/utils';
 
 defineProps({
-  amount: {
+  grossAmount: {
+    type: Number,
+    required: true,
+  },
+  gatewayFeeAmount: {
+    type: Number,
+    required: true,
+  },
+  netAmount: {
     type: Number,
     required: true,
   },
@@ -70,4 +95,10 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginC
   font-weight: 700;
   line-height: 1.2;
 }
+
+.withdraw-summary {
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  border-radius: 12px;
+  padding: 10px 12px;
+}
 </style>

+ 5 - 2
src/i18n/locales/en.json

@@ -435,7 +435,10 @@
       "anticipation_confirm_ok": "confirm",
       "default_client_name": "Client",
       "withdraw_confirm_title": "Confirm withdrawal",
-      "withdraw_confirm_message": "Do you want to withdraw {amount} to your account?"
+      "withdraw_confirm_message": "Do you want to confirm the withdrawal to your account?",
+      "withdraw_gross_amount_label": "Available balance",
+      "withdraw_fee_label": "Transfer fee",
+      "withdraw_net_amount_label": "Amount you will receive"
     }
   },
   "business": {
@@ -813,4 +816,4 @@
     "agenda": "Schedule",
     "profile": "Profile"
   }
-}
+}

+ 5 - 2
src/i18n/locales/es.json

@@ -433,7 +433,10 @@
       "anticipation_confirm_ok": "confirmar",
       "default_client_name": "Cliente",
       "withdraw_confirm_title": "Confirmar retiro",
-      "withdraw_confirm_message": "¿Desea retirar {amount} a su cuenta?"
+      "withdraw_confirm_message": "¿Desea confirmar el retiro a su cuenta?",
+      "withdraw_gross_amount_label": "Saldo disponible",
+      "withdraw_fee_label": "Tarifa de transferencia",
+      "withdraw_net_amount_label": "Monto que recibirá"
     }
   },
   "business": {
@@ -811,4 +814,4 @@
     "agenda": "Agenda",
     "profile": "Perfil"
   }
-}
+}

+ 5 - 2
src/i18n/locales/pt.json

@@ -438,7 +438,10 @@
       "anticipation_confirm_ok": "confirmar",
       "default_client_name": "Cliente",
       "withdraw_confirm_title": "Confirmar saque",
-      "withdraw_confirm_message": "Deseja sacar {amount} para sua conta?"
+      "withdraw_confirm_message": "Deseja confirmar o saque para sua conta?",
+      "withdraw_gross_amount_label": "Saldo disponível",
+      "withdraw_fee_label": "Taxa de transferência",
+      "withdraw_net_amount_label": "Valor que você receberá"
     }
   },
   "business": {
@@ -829,4 +832,4 @@
     "unread": "Não lidas",
     "mark_all_read": " Marcar todas como lidas"
   }
-}
+}

+ 27 - 6
src/pages/LoginPage.vue

@@ -178,7 +178,7 @@
             </q-card-section>
             <LoginStepSixPanel
               v-else-if="steps === 7"
-              v-model="stepSevenForm"
+              v-model="stepSixForm"
             />
 
             <div
@@ -191,7 +191,7 @@
             </div>
           </div>
 
-          <div v-if="!showSubStep && steps !== 7" class="flow-footer">
+          <div v-if="!showSubStep && steps !== 8" class="flow-footer">
             <q-btn
               color="primary-button"
               :label="actionLabel"
@@ -284,13 +284,20 @@ const stepFiveForm = ref({
 });
 
 const stepSixForm = ref({
+  account_type: 'checking',
+  bank: '',
+  branch_number: '',
+  branch_check_digit: '',
+  account_number: '',
+  account_check_digit: '',
+  pix_key: '',
   working_days: {},
 });
 
 const actionLabel = computed(() => {
   if (steps.value === 1) return t('provider.login.steps.step_1.action');
   if (steps.value === 2) return t('provider.login.steps.step_2.action');
-  if (steps.value === 6) return t('provider.login.steps.step_6.action');
+  if (steps.value === 7) return t('provider.login.steps.step_6.action');
   return t('provider.login.steps.step_3.action');
 });
 
@@ -317,9 +324,9 @@ const mapWorkingDays = () => {
   return mapped;
 };
 
-// const hasWorkingDaySelected = () => {
-//   return mapWorkingDays().length > 0;
-// };
+const hasWorkingDaySelected = () => {
+  return mapWorkingDays().length > 0;
+};
 
 const validateCurrentStep = async () => {
   const isValid = await loginForm.value?.validate();
@@ -342,6 +349,14 @@ const validateCurrentStep = async () => {
     }
   }
 
+  if (steps.value === 7 && !hasWorkingDaySelected()) {
+    $q.notify({
+      type: 'negative',
+      message: t('provider.login.steps.step_6.select_at_least_one'),
+    });
+    return false;
+  }
+
   return true;
 };
 
@@ -533,6 +548,12 @@ const onSubmit = async () => {
         break;
       }
       case 6: {
+        if (await validateCurrentStep()) {
+          steps.value = 7;
+        }
+        break;
+      }
+      case 7: {
         if (await validateCurrentStep()) {
           await registerUserAndProvider();
         }

+ 46 - 6
src/pages/payments/PaymentsPage.vue

@@ -73,7 +73,7 @@
               class="btn-withdraw"
               :label="$t('provider.payments.btn_withdraw')"
               :loading="withdrawLoading"
-              :disable="saldoDisponivel <= 0 || withdrawLoading"
+              :disable="!canWithdraw"
               @click="onSacar"
             />
           </div>
@@ -166,12 +166,12 @@
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue';
+import { computed, ref, onMounted } from 'vue';
 import { useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { formatCurrency } from 'src/helpers/utils';
 import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js';
-import { getBalance, requestWithdrawal } from 'src/api/providerWithdrawal';
+import { getBalance, getWithdrawalFees, requestWithdrawal } from 'src/api/providerWithdrawal';
 import { getPaymentSplits } from 'src/api/paymentSplit';
 import MovimentacoesDialog from 'src/components/payments/MovimentacoesDialog.vue';
 import WithdrawConfirmDialog from 'src/components/payments/WithdrawConfirmDialog.vue';
@@ -206,6 +206,7 @@ const earningsByPeriod = ref({
 
 const saldoDisponivel = ref(0);
 const saldoALiberar = ref(0);
+const paymentTransferFeeAmount = ref(3.67);
 const services = ref([]);
 
 const parseAmount = (value) => {
@@ -213,6 +214,18 @@ const parseAmount = (value) => {
   return Number.isFinite(amount) ? amount : 0;
 };
 
+const roundMoney = (value) => Math.round((parseAmount(value) + Number.EPSILON) * 100) / 100;
+
+const withdrawalNetAmount = computed(() => (
+  Math.max(0, roundMoney(saldoDisponivel.value - paymentTransferFeeAmount.value))
+));
+
+const canWithdraw = computed(() => (
+  saldoDisponivel.value > 0
+  && withdrawalNetAmount.value > 0
+  && !withdrawLoading.value
+));
+
 const splitAmount = (split) => (
   parseAmount(split.provider_amount ?? split.net_amount ?? split.gross_amount)
 );
@@ -264,9 +277,10 @@ const sumSplits = (splits, predicate) => (
 const loadData = async () => {
   servicesLoading.value = true;
   try {
-    const [balance, splits] = await Promise.all([
+    const [balance, splits, fees] = await Promise.all([
       getBalance(),
       getPaymentSplits(),
+      loadWithdrawalFees(),
     ]);
     const paymentSplits = splits || [];
     const availableFromSplits = sumSplits(paymentSplits, isSplitAvailable);
@@ -277,6 +291,12 @@ const loadData = async () => {
 
     saldoDisponivel.value = parseAmount(balance?.available) || availableFromSplits;
     saldoALiberar.value = parseAmount(balance?.pending) || pendingFromSplits;
+    paymentTransferFeeAmount.value = parseAmount(
+      fees?.payment_transfer_fee_amount
+      ?? fees?.gateway_fee_amount
+      ?? fees?.fee_amount
+      ?? fees?.amount
+    ) || paymentTransferFeeAmount.value;
 
     services.value = paymentSplits.map((s) => ({
       id: s.id,
@@ -298,14 +318,34 @@ const loadData = async () => {
   }
 };
 
+const loadWithdrawalFees = async () => {
+  try {
+    return await getWithdrawalFees();
+  } catch {
+    return { payment_transfer_fee_amount: paymentTransferFeeAmount.value };
+  }
+};
+
 const onSacar = () => {
+  const grossAmount = roundMoney(saldoDisponivel.value);
+  const gatewayFeeAmount = roundMoney(paymentTransferFeeAmount.value);
+  const netAmount = roundMoney(withdrawalNetAmount.value);
+
   $q.dialog({
     component: WithdrawConfirmDialog,
-    componentProps: { amount: saldoDisponivel.value },
+    componentProps: {
+      grossAmount,
+      gatewayFeeAmount,
+      netAmount,
+    },
   }).onOk(async () => {
     withdrawLoading.value = true;
     try {
-      await requestWithdrawal();
+      await requestWithdrawal({
+        gross_amount: grossAmount,
+        gateway_fee_amount: gatewayFeeAmount,
+        net_amount: netAmount,
+      });
       saldoDisponivel.value = 0;
       // notification handled by axios interceptor
     } catch (error) {