commit 617dd2bc3efbe851bd7c009ce9b8488236fa910b
parent c0cf3c03f69a47b3713fdba5eadecae30e3bf22b
Author: Sebastian <sebasjm@gmail.com>
Date: Fri, 24 Oct 2025 19:06:09 -0300
bank
Diffstat:
5 files changed, 263 insertions(+), 240 deletions(-)
diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts
@@ -48,7 +48,7 @@ export type TransCalc = {
credit: AmountJson;
beforeFee: AmountJson;
};
-export type TransferCalculation = TransCalc | "amount-is-too-small";
+export type TransferCalculation = TransCalc | undefined;
type EstimatorFunction = (
amount: AmountJson,
fee: AmountJson,
diff --git a/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx b/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx
@@ -48,6 +48,9 @@ import { DescribeConversion } from "./admin/ConversionClassList.js";
import { doAutoFocus, InputAmount } from "./PaytoWireTransferForm.js";
import { ConversionForm } from "./regional/ConversionConfig.js";
import { AccessToken } from "@gnu-taler/taler-util";
+import { TalerErrorCode } from "@gnu-taler/taler-util";
+import { opFixedSuccess } from "@gnu-taler/taler-util";
+import { AmountJson } from "@gnu-taler/taler-util";
interface Props {
classId: number;
@@ -184,31 +187,7 @@ function Form({
}
};
- async function doUpdateClass1() {
- if (!creds) return;
- if (status.status !== "ok") {
- console.log("can submit due to form error", status.errors);
- return;
- }
-
- await bank.updateConversionRateClass(creds.token, classId, {
- name: status.result.name,
- description: status.result.description,
-
- cashin_fee: status.result.conv.cashin_fee,
- cashin_min_amount: status.result.conv.cashin_min_amount,
- cashin_ratio: status.result.conv.cashin_ratio,
- cashin_rounding_mode: status.result.conv.cashin_rounding_mode,
-
- cashout_fee: status.result.conv.cashout_fee,
- cashout_min_amount: status.result.conv.cashout_min_amount,
- cashout_ratio: status.result.conv.cashout_ratio,
- cashout_rounding_mode: status.result.conv.cashout_rounding_mode,
- });
- setSection("detail");
- }
-
- const updateRequest: TalerCorebankApi.ConversionRateClassInput | undefined =
+ const input: TalerCorebankApi.ConversionRateClassInput | undefined =
status.status === "fail"
? undefined
: {
@@ -226,25 +205,52 @@ function Form({
cashout_rounding_mode: status.result.conv.cashout_rounding_mode,
};
- const updateClassTemplate = safeFunctionHandler(
- (
- token: AccessToken,
- updateRequest: TalerCorebankApi.ConversionRateClassInput,
- ) => bank.updateConversionRateClass(token, classId, updateRequest),
+ const updateClass = safeFunctionHandler(
+ bank.updateConversionRateClass,
+ !creds || !input ? undefined : [creds.token, classId, input],
);
-
- updateClassTemplate.onSuccess = () => {
+ updateClass.onSuccess = () => {
setSection("detail");
};
- updateClassTemplate.onFail = (fail) => {
+ updateClass.onFail = (fail) => {
switch (fail.case) {
- default:
+ case HttpStatusCode.Unauthorized:
+ return i18n.str``;
+ case HttpStatusCode.Forbidden:
+ return i18n.str``;
+ case HttpStatusCode.NotFound:
+ return i18n.str``;
+ case HttpStatusCode.NotImplemented:
+ return i18n.str``;
+ case TalerErrorCode.BANK_NAME_REUSE:
return i18n.str``;
}
};
- const updateDetails = updateClassTemplate.lambda(
- (t: AccessToken, r: TalerCorebankApi.ConversionRateClassInput) => [t, r],
+ const updateRequest: TalerCorebankApi.ConversionRateClassInput | undefined =
+ status.status === "fail"
+ ? undefined
+ : {
+ name: status.result.name,
+ description: status.result.description,
+
+ cashin_fee: status.result.conv.cashin_fee,
+ cashin_min_amount: status.result.conv.cashin_min_amount,
+ cashin_ratio: status.result.conv.cashin_ratio,
+ cashin_rounding_mode: status.result.conv.cashin_rounding_mode,
+
+ cashout_fee: status.result.conv.cashout_fee,
+ cashout_min_amount: status.result.conv.cashout_min_amount,
+ cashout_ratio: status.result.conv.cashout_ratio,
+ cashout_rounding_mode: status.result.conv.cashout_rounding_mode,
+ };
+
+ const updateDetails = updateClass.lambda(
+ (
+ t: AccessToken,
+ id: number,
+ r: TalerCorebankApi.ConversionRateClassInput,
+ ) => [t, id, r],
!creds ||
!updateRequest ||
section !== "detail" ||
@@ -253,46 +259,87 @@ function Form({
(status.result.name === initalState.name &&
status.result.description === initalState.description)
? undefined
- : [creds.token, updateRequest],
+ : [creds.token, classId, updateRequest],
);
-
- const doUpdateDetails1 =
- !creds ||
- section !== "detail" ||
- status.errors?.name ||
- status.errors?.description ||
- (status.result.name === initalState.name &&
- status.result.description === initalState.description)
- ? undefined
- : doUpdateClass2;
- const doUpdateCashin1 =
+ // const doUpdateDetails1 =
+ // !creds ||
+ // section !== "detail" ||
+ // status.errors?.name ||
+ // status.errors?.description ||
+ // (status.result.name === initalState.name &&
+ // status.result.description === initalState.description)
+ // ? undefined
+ // : doUpdateClass2;
+
+ const updateCashin = updateClass.lambda(
+ (
+ t: AccessToken,
+ id: number,
+ r: TalerCorebankApi.ConversionRateClassInput,
+ ) => [t, id, r],
!creds ||
- section !== "cashin" ||
- status.errors?.conv?.cashin_fee ||
- status.errors?.conv?.cashin_min_amount ||
- status.errors?.conv?.cashin_ratio ||
- status.errors?.conv?.cashin_rounding_mode
+ !updateRequest ||
+ section !== "cashin" ||
+ status.errors?.conv?.cashin_fee ||
+ status.errors?.conv?.cashin_min_amount ||
+ status.errors?.conv?.cashin_ratio ||
+ status.errors?.conv?.cashin_rounding_mode
? undefined
- : doUpdateClass2;
-
- const doUpdateCashout1 =
+ : [creds.token, classId, updateRequest],
+ );
+ // const doUpdateCashin1 =
+ // !creds ||
+ // section !== "cashin" ||
+ // status.errors?.conv?.cashin_fee ||
+ // status.errors?.conv?.cashin_min_amount ||
+ // status.errors?.conv?.cashin_ratio ||
+ // status.errors?.conv?.cashin_rounding_mode
+ // ? undefined
+ // : doUpdateClass2;
+
+ const updateCashout = updateClass.lambda(
+ (
+ t: AccessToken,
+ id: number,
+ r: TalerCorebankApi.ConversionRateClassInput,
+ ) => [t, id, r],
!creds ||
- section !== "cashout" ||
- // no errors on fields
- status.errors?.conv?.cashout_fee ||
- status.errors?.conv?.cashout_min_amount ||
- status.errors?.conv?.cashout_ratio ||
- status.errors?.conv?.cashout_rounding_mode ||
- // at least on field changed
- (status.result?.conv?.cashout_fee === initalState.conv.cashout_fee &&
- status.result?.conv?.cashout_min_amount ===
- initalState.conv.cashout_min_amount &&
- status.result?.conv?.cashout_ratio === initalState.conv.cashout_ratio &&
- status.result?.conv?.cashout_rounding_mode ===
- initalState.conv.cashout_rounding_mode)
+ !updateRequest ||
+ section !== "cashout" ||
+ // no errors on fields
+ status.errors?.conv?.cashout_fee ||
+ status.errors?.conv?.cashout_min_amount ||
+ status.errors?.conv?.cashout_ratio ||
+ status.errors?.conv?.cashout_rounding_mode ||
+ // at least on field changed
+ (status.result?.conv?.cashout_fee === initalState.conv.cashout_fee &&
+ status.result?.conv?.cashout_min_amount ===
+ initalState.conv.cashout_min_amount &&
+ status.result?.conv?.cashout_ratio === initalState.conv.cashout_ratio &&
+ status.result?.conv?.cashout_rounding_mode ===
+ initalState.conv.cashout_rounding_mode)
? undefined
- : doUpdateClass2;
+ : [creds.token, classId, updateRequest],
+ );
+
+ // const doUpdateCashout1 =
+ // !creds ||
+ // section !== "cashout" ||
+ // // no errors on fields
+ // status.errors?.conv?.cashout_fee ||
+ // status.errors?.conv?.cashout_min_amount ||
+ // status.errors?.conv?.cashout_ratio ||
+ // status.errors?.conv?.cashout_rounding_mode ||
+ // // at least on field changed
+ // (status.result?.conv?.cashout_fee === initalState.conv.cashout_fee &&
+ // status.result?.conv?.cashout_min_amount ===
+ // initalState.conv.cashout_min_amount &&
+ // status.result?.conv?.cashout_ratio === initalState.conv.cashout_ratio &&
+ // status.result?.conv?.cashout_rounding_mode ===
+ // initalState.conv.cashout_rounding_mode)
+ // ? undefined
+ // : doUpdateClass2;
const default_rate = conversionInfo.conversion_rate;
@@ -645,7 +692,9 @@ function Form({
/>
)}
- {section == "test" && <TestConversionClass classId={classId} />}
+ {section == "test" && (
+ <TestConversionClass classId={classId} info={conversionInfo} />
+ )}
<div class="flex items-center justify-between mt-4 gap-x-6 border-t border-gray-900/10 px-4 py-4">
<a
@@ -685,7 +734,7 @@ function Form({
type="submit"
name="update conversion"
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={updateDelete}
+ onClick={updateDetails}
>
<i18n.Translate>Update</i18n.Translate>
</ButtonBetter>
@@ -826,14 +875,15 @@ export function createFormValidator(
};
}
-function TestConversionClass({ classId }: { classId: number }): VNode {
+function TestConversionClass({
+ classId,
+ info,
+}: {
+ classId: number;
+ info: TalerBankConversionApi.TalerConversionInfoConfig;
+}): VNode {
const { i18n } = useTranslationContext();
const [notification, safeFunctionHandler] = useLocalNotificationBetter();
- const result = useConversionInfo();
- const info =
- result && !(result instanceof TalerError) && result.type === "ok"
- ? result.body
- : undefined;
const { estimateByDebit: calculateCashoutFromDebit } =
useCashoutEstimatorForClass(classId);
@@ -848,65 +898,57 @@ function TestConversionClass({ classId }: { classId: number }): VNode {
cashout: TransferCalculation;
}>();
- useEffect(() => {
- async function doAsync() {
- await handleError(async () => {
- if (!info) return;
- if (!amount || error) return;
- const in_amount = Amounts.parseOrThrow(
- `${info.fiat_currency}:${amount}`,
- );
- const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee);
- const respCashin = await calculateCashinFromDebit(in_amount, in_fee);
-
- const cashin =
- respCashin.type === "ok"
- ? respCashin.body
- : respCashin.case === HttpStatusCode.Conflict
- ? ("amount-is-too-small" as const)
- : undefined;
-
- if (!cashin || cashin === "amount-is-too-small") {
- setCalc(undefined); // silent failure
- return;
- }
-
- const out_fee = Amounts.parseOrThrow(info.conversion_rate.cashout_fee);
- const respCashout = await calculateCashoutFromDebit(
- cashin.credit,
- out_fee,
- );
-
- const cashout =
- respCashout.type === "ok"
- ? respCashout.body
- : respCashout.case === HttpStatusCode.Conflict
- ? ("amount-is-too-small" as const)
- : undefined;
-
- if (!cashout) {
- setCalc(undefined); // silent failure
- return;
- }
+ const in_amount = !amount
+ ? undefined
+ : Amounts.parseOrThrow(`${info.fiat_currency}:${amount}`);
+
+ const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee);
+ const out_fee = Amounts.parseOrThrow(info.conversion_rate.cashout_fee);
+
+ const calculate = safeFunctionHandler(
+ async (amount: AmountJson) => {
+ const respCashin = await calculateCashinFromDebit(amount, in_fee);
+ if (respCashin.type === "fail") {
+ return respCashin;
+ }
+ const cashin = respCashin.body;
+ const respCashout = await calculateCashoutFromDebit(
+ cashin.credit,
+ out_fee,
+ );
+ if (respCashout.type === "fail") {
+ return respCashout;
+ }
+ const cashout = respCashout.body;
+ return opFixedSuccess({ cashin, cashout });
+ },
+ !in_amount || !!error ? undefined : [in_amount],
+ );
- setCalc({ cashin, cashout });
- });
+ calculate.onSuccess = (resp) => setCalc(resp.body);
+ calculate.onFail = (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str`The server didn't undertand the request.`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`The amount is too small`;
+ case HttpStatusCode.NotImplemented:
+ return i18n.str`Conversion is not implemented.`;
+ case TalerErrorCode.GENERIC_PARAMETER_MISSING:
+ return i18n.str`At least debit or credit needs to be provided`;
+ case TalerErrorCode.GENERIC_PARAMETER_MALFORMED:
+ return i18n.str`The amount is malfored`;
+ case TalerErrorCode.GENERIC_CURRENCY_MISMATCH:
+ return i18n.str`The currency is not supported`;
}
- doAsync();
- }, [amount]);
+ };
- if (!info) {
- return <Loading />;
- }
+ useEffect(() => {
+ calculate.call();
+ }, [amount]);
- const cashinCalc =
- calculationResult?.cashin === "amount-is-too-small"
- ? undefined
- : calculationResult?.cashin;
- const cashoutCalc =
- calculationResult?.cashout === "amount-is-too-small"
- ? undefined
- : calculationResult?.cashout;
+ const cashinCalc = calculationResult?.cashin;
+ const cashoutCalc = calculationResult?.cashout;
return (
<Fragment>
diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
@@ -26,6 +26,7 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
+ ButtonBetter,
ErrorLoading,
InternationalizationAPI,
Loading,
@@ -59,6 +60,9 @@ import { InputAmount, RenderAmount } from "../PaytoWireTransferForm.js";
import { ProfileNavigation } from "../ProfileNavigation.js";
import { DescribeConversion } from "../admin/ConversionClassList.js";
+import { opEmptySuccess } from "@gnu-taler/taler-util";
+import { opFixedSuccess } from "@gnu-taler/taler-util";
+import { TalerErrorCode } from "@gnu-taler/taler-util";
const TALER_SCREEN_ID = 126;
@@ -164,53 +168,52 @@ function useComponentState({
cashout: TransferCalculation;
}>();
- useEffect(() => {
- async function doAsync() {
- await handleError(async () => {
- if (!info) return;
- if (!form.amount?.value || form.amount.error) return;
- const in_amount = Amounts.parseOrThrow(
- `${info.fiat_currency}:${form.amount.value}`,
- );
- const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee);
- const respCashin = await calculateCashinFromDebit(in_amount, in_fee);
-
- const cashin =
- respCashin.type === "ok"
- ? respCashin.body
- : respCashin.case === HttpStatusCode.Conflict
- ? ("amount-is-too-small" as const)
- : undefined;
-
- if (!cashin || cashin === "amount-is-too-small") {
- setCalc(undefined);
- return;
- }
- // const out_amount = Amounts.parseOrThrow(`${info.regional_currency}:${form.amount.value}`)
- const out_fee = Amounts.parseOrThrow(
- info.conversion_rate.cashout_fee,
- );
- const respCashout = await calculateCashoutFromDebit(
- cashin.credit,
- out_fee,
- );
-
- const cashout =
- respCashout.type === "ok"
- ? respCashout.body
- : respCashout.case === HttpStatusCode.Conflict
- ? ("amount-is-too-small" as const)
- : undefined;
-
- if (!cashout) {
- setCalc(undefined); // silent failure
- return;
- }
-
- setCalc({ cashin, cashout });
- });
+ const in_amount = !form.amount
+ ? undefined
+ : Amounts.parseOrThrow(`${info.fiat_currency}:${form.amount.value}`);
+
+ const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee);
+ const out_fee = Amounts.parseOrThrow(info.conversion_rate.cashout_fee);
+
+ const calculate = safeFunctionHandler(
+ async (amount: AmountJson) => {
+ const respCashin = await calculateCashinFromDebit(amount, in_fee);
+ if (respCashin.type === "fail") {
+ return respCashin;
+ }
+ const cashin = respCashin.body;
+ const respCashout = await calculateCashoutFromDebit(
+ cashin.credit,
+ out_fee,
+ );
+ if (respCashout.type === "fail") {
+ return respCashout;
+ }
+ const cashout = respCashout.body;
+ return opFixedSuccess({ cashin, cashout });
+ },
+ !in_amount || status.status === "fail" ? undefined : [in_amount],
+ );
+ calculate.onSuccess = (resp) => setCalc(resp.body);
+ calculate.onFail = (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str`The server didn't undertand the request.`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`The amount is too small`;
+ case HttpStatusCode.NotImplemented:
+ return i18n.str`Conversion is not implemented.`;
+ case TalerErrorCode.GENERIC_PARAMETER_MISSING:
+ return i18n.str`At least debit or credit needs to be provided`;
+ case TalerErrorCode.GENERIC_PARAMETER_MALFORMED:
+ return i18n.str`The amount is malfored`;
+ case TalerErrorCode.GENERIC_CURRENCY_MISMATCH:
+ return i18n.str`The currency is not supported`;
}
- doAsync();
+ };
+
+ useEffect(() => {
+ calculate.call();
}, [
form.amount?.value,
form.conv?.cashin_fee?.value,
@@ -220,53 +223,27 @@ function useComponentState({
const [section, setSection] = useState<"detail" | "cashout" | "cashin">(
"detail",
);
- const cashinCalc =
- calculationResult?.cashin === "amount-is-too-small"
- ? undefined
- : calculationResult?.cashin;
- const cashoutCalc =
- calculationResult?.cashout === "amount-is-too-small"
- ? undefined
- : calculationResult?.cashout;
+ const cashinCalc = calculationResult?.cashin;
+ const cashoutCalc = calculationResult?.cashout;
+ const update = safeFunctionHandler(
+ conversion.updateConversionRate,
+ !creds || status.status === "fail"
+ ? undefined
+ : [creds.token, status.result.conv],
+ );
-
- async function doUpdate() {
- if (!creds) return;
- await handleError(async () => {
- if (status.status === "fail") return;
- const resp = await conversion.updateConversionRate(
- creds.token,
- status.result.conv,
- );
- if (resp.type === "ok") {
- setSection("detail");
- } else {
- switch (resp.case) {
- case HttpStatusCode.Unauthorized: {
- return notify({
- type: "error",
- title: i18n.str`Wrong credentials`,
- description: resp.detail?.hint as TranslatedString,
- debug: resp.detail,
- when: AbsoluteTime.now(),
- });
- }
- case HttpStatusCode.NotImplemented: {
- return notify({
- type: "error",
- title: i18n.str`Conversion is disabled`,
- description: resp.detail?.hint as TranslatedString,
- debug: resp.detail,
- when: AbsoluteTime.now(),
- });
- }
- default:
- assertUnreachable(resp);
- }
- }
- });
- }
+ update.onSuccess = () => {
+ setSection("detail");
+ };
+ update.onFail = (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`Wrong credentials`;
+ case HttpStatusCode.NotImplemented:
+ return i18n.str`Conversion is disabled`;
+ }
+ };
const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio);
const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio);
@@ -607,16 +584,14 @@ function useComponentState({
</a>
{section == "cashin" || section == "cashout" ? (
<Fragment>
- <button
+ <ButtonBetter
type="submit"
name="update conversion"
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={async () => {
- doUpdate();
- }}
+ onClick={update}
>
<i18n.Translate>Update</i18n.Translate>
- </button>
+ </ButtonBetter>
</Fragment>
) : (
<div />
diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
@@ -284,12 +284,18 @@ function CreateCashoutInternal({
conversionCalculator.onSuccess = (success) => setCalculation(success.body);
conversionCalculator.onFail = (fail) => {
switch (fail.case) {
- case HttpStatusCode.Conflict:
- return i18n.str`The amount is too small.`;
case HttpStatusCode.BadRequest:
- return i18n.str`Server didn't like our request`;
+ return i18n.str`The server didn't undertand the request.`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`The amount is too small`;
case HttpStatusCode.NotImplemented:
- return i18n.str`Conversion is not enabled.`;
+ return i18n.str`Conversion is not implemented.`;
+ case TalerErrorCode.GENERIC_PARAMETER_MISSING:
+ return i18n.str`At least debit or credit needs to be provided`;
+ case TalerErrorCode.GENERIC_PARAMETER_MALFORMED:
+ return i18n.str`The amount is malfored`;
+ case TalerErrorCode.GENERIC_CURRENCY_MISMATCH:
+ return i18n.str`The currency is not supported`;
}
};
@@ -297,8 +303,7 @@ function CreateCashoutInternal({
conversionCalculator.call();
}, [form.amount, form.isDebit, notZero, higerThanMin, rate.cashout_fee]);
- const calc =
- calculationResult === "amount-is-too-small" ? zeroCalc : calculationResult;
+ const calc = !calculationResult ? zeroCalc : calculationResult;
const balanceAfter = IntAmounts.toIntAmount(
account.balance,
@@ -314,7 +319,7 @@ function CreateCashoutInternal({
? i18n.str`Required`
: !inputAmount
? i18n.str`Invalid`
- : calculationResult === "amount-is-too-small"
+ : !calculationResult
? i18n.str`Amount needs to be higher`
: Amounts.isZero(
balanceLimit
diff --git a/packages/taler-util/src/http-client/bank-conversion.ts b/packages/taler-util/src/http-client/bank-conversion.ts
@@ -28,6 +28,7 @@ import {
carefullyParseConfig,
opEmptySuccess,
opKnownHttpFailure,
+ opKnownTalerFailure,
opSuccessFromHttp,
opUnknownHttpFailure,
} from "../operation.js";
@@ -163,11 +164,11 @@ export class TalerBankConversionHttpClient {
const details = codecForTalerErrorDetail().decode(body);
switch (details.code) {
case TalerErrorCode.GENERIC_PARAMETER_MISSING:
- return opKnownHttpFailure(resp.status, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.GENERIC_PARAMETER_MALFORMED:
- return opKnownHttpFailure(resp.status, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.GENERIC_CURRENCY_MISMATCH:
- return opKnownHttpFailure(resp.status, resp);
+ return opKnownTalerFailure(details.code, details);
default:
return opUnknownHttpFailure(resp, details);
}