All files / src/entities/refund/model refundStore.ts

93.65% Statements 59/63
86.53% Branches 45/52
100% Functions 13/13
93.44% Lines 57/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157                        2x                               1x 1x                 30x               10x     10x       2x 8x 8x 8x 8x 8x 8x 8x     10x 10x 10x 10x 10x 9x 9x 9x 9x 9x 9x 9x 9x     10x       10x       21x           19x       7x 1x         6x 6x 1x   5x                 10x 10x     10x 10x 10x 10x 10x 8x 1x   7x 7x 7x   3x     3x 2x   1x 3x   10x 9x         8x                    
import { ref } from "vue";
import { defineStore } from "pinia";
 
import {
  ApiError,
  createApiClient,
  createPaymentApi,
  type BusinessId,
  type PaymentApi,
  type Refund,
} from "@plain-journal/foundation";
 
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL?.trim() ?? "";
 
export interface RefundAccessContext {
  authenticated: boolean;
  ownerId: BusinessId | null;
  accessToken: string | null;
}
 
interface ActiveRefundAccess {
  ownerId: BusinessId;
  accessToken: string;
  revision: number;
}
 
class RefundResponseMismatchError extends Error {
  constructor() {
    super("Payment 已响应,但返回的退款事实与本次账户或售后记录不一致。");
    this.name = "RefundResponseMismatchError";
  }
}
 
function isActiveContext(context: RefundAccessContext): context is {
  authenticated: true;
  ownerId: BusinessId;
  accessToken: string;
} {
  return context.authenticated
    && typeof context.ownerId === "string"
    && context.ownerId.length > 0
    && typeof context.accessToken === "string"
    && context.accessToken.length > 0;
}
 
function paymentApi(accessToken: string): PaymentApi {
  return createPaymentApi(createApiClient({
    baseUrl: apiBaseUrl,
    timeoutMs: 8000,
    tokenProvider: () => accessToken,
  }));
}
 
export const useRefundsStore = defineStore("customer-refunds", () => {
  const refunds = ref<Refund[]>([]);
  const loadingAfterSaleNo = ref<string | null>(null);
  const error = ref<string | null>(null);
  const activeOwnerId = ref<BusinessId | null>(null);
  let activeAccessToken: string | null = null;
  let accessRevision = 0;
  let requestRevision = 0;
 
  function synchronizeAccess(context: RefundAccessContext): ActiveRefundAccess | null {
    const nextOwnerId = isActiveContext(context) ? context.ownerId : null;
    const nextAccessToken = isActiveContext(context) ? context.accessToken : null;
    const ownerChanged = activeOwnerId.value !== nextOwnerId;
    const tokenChanged = activeAccessToken !== nextAccessToken;
    if (ownerChanged || tokenChanged) {
      activeOwnerId.value = nextOwnerId;
      activeAccessToken = nextAccessToken;
      accessRevision += 1;
      requestRevision += 1;
      loadingAfterSaleNo.value = null;
      error.value = null;
      Eif (ownerChanged) {
        refunds.value = [];
      }
    }
    Iif (!isActiveContext(context)) {
      refunds.value = [];
      return null;
    }
    return { ownerId: context.ownerId, accessToken: context.accessToken, revision: accessRevision };
  }
 
  function accessIsCurrent(access: ActiveRefundAccess): boolean {
    return access.revision === accessRevision
      && access.ownerId === activeOwnerId.value
      && access.accessToken === activeAccessToken;
  }
 
  function forAfterSale(afterSaleNo: string): Refund | null {
    return refunds.value.find((value) => value.afterSaleNo === afterSaleNo) ?? null;
  }
 
  function verifyFact(access: ActiveRefundAccess, value: Refund, afterSaleNo: string) {
    if (value.userId !== access.ownerId || value.afterSaleNo !== afterSaleNo) {
      throw new RefundResponseMismatchError();
    }
  }
 
  function upsert(value: Refund) {
    const index = refunds.value.findIndex((candidate) => candidate.refundNo === value.refundNo);
    if (index >= 0) {
      refunds.value[index] = value;
    } else {
      refunds.value.unshift(value);
    }
  }
 
  async function loadByAfterSale(
    context: RefundAccessContext,
    afterSaleNo: string,
    silentNotFound = true,
  ): Promise<Refund | null> {
    const access = synchronizeAccess(context);
    Iif (!access) {
      return null;
    }
    const currentRevision = ++requestRevision;
    loadingAfterSaleNo.value = afterSaleNo;
    error.value = null;
    try {
      const value = await paymentApi(access.accessToken).refundByAfterSale(afterSaleNo);
      if (!accessIsCurrent(access) || currentRevision !== requestRevision) {
        return null;
      }
      verifyFact(access, value, afterSaleNo);
      upsert(value);
      return value;
    } catch (cause) {
      Iif (!accessIsCurrent(access) || currentRevision !== requestRevision) {
        return null;
      }
      if (silentNotFound && cause instanceof ApiError && cause.status === 404) {
        return null;
      }
      error.value = cause instanceof Error ? cause.message : "退款事实暂时无法读取。";
      return null;
    } finally {
      if (accessIsCurrent(access) && currentRevision === requestRevision) {
        loadingAfterSaleNo.value = null;
      }
    }
  }
 
  return {
    refunds,
    loadingAfterSaleNo,
    error,
    activeOwnerId,
    synchronizeAccess,
    forAfterSale,
    loadByAfterSale,
  };
});