Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions src/actions/sponsor-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2279,22 +2279,26 @@ export const querySummitSponsorships = _.debounce(
DEBOUNCE_WAIT
);

export const querySummitAddons = _.debounce(
async (input, summitId, callback) => {
const accessToken = await getAccessTokenSafely();
const endpoint = URI(
`${window.API_BASE_URL}/api/v1/summits/${summitId}/add-ons/metadata`
);
endpoint.addQuery("access_token", accessToken);
fetch(endpoint)
.then(fetchResponseHandler)
.then((data) => {
callback(data);
})
.catch(fetchErrorHandler);
},
DEBOUNCE_WAIT
);
export const querySummitAddons = async (
summitId,
callback
) => {
const accessToken = await getAccessTokenSafely();
const endpoint = URI(
`${window.API_BASE_URL}/api/v1/summits/${summitId}/add-ons/metadata`
);
endpoint.addQuery("access_token", accessToken);
endpoint.addQuery("page", 1);
endpoint.addQuery("per_page", MAX_PER_PAGE);

return fetch(endpoint)
.then(fetchResponseHandler)
.then((data) => callback(data))
.catch((error) => {
fetchErrorHandler(error);
return [];
});
};

export const querySponsorAddons = async (
summitId,
Expand Down
141 changes: 139 additions & 2 deletions src/actions/sponsor-cart-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,29 @@ import {
getRequest,
deleteRequest,
putRequest,
postRequest,
startLoading,
stopLoading
} from "openstack-uicore-foundation/lib/utils/actions";

import T from "i18n-react";
import { escapeFilterValue, getAccessTokenSafely } from "../utils/methods";
import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions";
import { ERROR_CODE_404 } from "../utils/constants";
import {
DEFAULT_CURRENT_PAGE,
DEFAULT_ORDER_DIR,
DEFAULT_PER_PAGE,
ERROR_CODE_404
} from "../utils/constants";

export const REQUEST_SPONSOR_CART = "REQUEST_SPONSOR_CART";
export const RECEIVE_SPONSOR_CART = "RECEIVE_SPONSOR_CART";
export const SPONSOR_CART_FORM_DELETED = "SPONSOR_CART_FORM_DELETED";
export const SPONSOR_CART_FORM_LOCKED = "SPONSOR_CART_FORM_LOCKED";
export const REQUEST_CART_AVAILABLE_FORMS = "REQUEST_CART_AVAILABLE_FORMS";
export const RECEIVE_CART_AVAILABLE_FORMS = "RECEIVE_CART_AVAILABLE_FORMS";
export const REQUEST_CART_SPONSOR_FORM = "REQUEST_CART_SPONSOR_FORM";
export const RECEIVE_CART_SPONSOR_FORM = "RECEIVE_CART_SPONSOR_FORM";
export const FORM_CART_SAVED = "FORM_CART_SAVED";

const customErrorHandler = (err, res) => (dispatch, state) => {
const code = err.status;
Expand Down Expand Up @@ -163,3 +173,130 @@ export const unlockSponsorCartForm = (formId) => async (dispatch, getState) => {
dispatch(stopLoading());
});
};

export const getSponsorFormsForCart =
(
term = "",
currentPage = DEFAULT_CURRENT_PAGE,
order = "id",
orderDir = DEFAULT_ORDER_DIR
) =>
async (dispatch, getState) => {
const { currentSummitState } = getState();
const { currentSummit } = currentSummitState;
const accessToken = await getAccessTokenSafely();
const filter = ["has_items==1"];

dispatch(startLoading());

if (term) {
const escapedTerm = escapeFilterValue(term);
filter.push(`name=@${escapedTerm},code=@${escapedTerm}`);
}

const params = {
page: currentPage,
fields: "id,code,name,items",
per_page: DEFAULT_PER_PAGE,
access_token: accessToken
};

if (filter.length > 0) {
params["filter[]"] = filter;
}

// order
if (order != null && orderDir != null) {
const orderDirSign = orderDir === 1 ? "" : "-";
params.order = `${orderDirSign}${order}`;
}

return getRequest(
createAction(REQUEST_CART_AVAILABLE_FORMS),
createAction(RECEIVE_CART_AVAILABLE_FORMS),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/show-forms`,
authErrorHandler,
{ term, order, orderDir, currentPage }
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Comment on lines +220 to +222
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use .finally() to ensure loading state is cleared on error.

If the request fails after authErrorHandler runs, stopLoading won't be called, potentially leaving the UI in a loading state. Other functions in this file use .finally() for this purpose.

🛠️ Suggested fix
-    )(params)(dispatch).then(() => {
-      dispatch(stopLoading());
-    });
+    )(params)(dispatch)
+      .catch((err) => {
+        console.error(err);
+      })
+      .finally(() => {
+        dispatch(stopLoading());
+      });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
)(params)(dispatch)
.catch((err) => {
console.error(err);
})
.finally(() => {
dispatch(stopLoading());
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/sponsor-cart-actions.js` around lines 220 - 222, The final
.then() call that dispatches stopLoading() can be skipped on errors; change the
promise chain that currently does ")(params)(dispatch).then(() => {
dispatch(stopLoading()); });" to use .finally(() => { dispatch(stopLoading());
}); so stopLoading is always dispatched regardless of success or failure (this
is the same pattern used elsewhere in this file and should be applied to the
thunk invocation that follows authErrorHandler).

};

// get sponsor show form by id USING V2 API
export const getSponsorForm = (formId) => async (dispatch, getState) => {
const { currentSummitState } = getState();
const { currentSummit } = currentSummitState;
const accessToken = await getAccessTokenSafely();

dispatch(startLoading());

const params = {
access_token: accessToken
};

return getRequest(
createAction(REQUEST_CART_SPONSOR_FORM),
createAction(RECEIVE_CART_SPONSOR_FORM),
`${window.PURCHASES_API_URL}/api/v2/summits/${currentSummit.id}/show-forms/${formId}`,
authErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Comment on lines +242 to +244
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same issue: Use .finally() for reliable loading state cleanup.

Consistent with the previous comment, replace .then() with .finally() to ensure stopLoading is called regardless of success or failure.

🛠️ Suggested fix
-  )(params)(dispatch).then(() => {
-    dispatch(stopLoading());
-  });
+  )(params)(dispatch)
+    .catch((err) => {
+      console.error(err);
+    })
+    .finally(() => {
+      dispatch(stopLoading());
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/sponsor-cart-actions.js` around lines 242 - 244, The promise
chain that currently calls dispatch(stopLoading()) inside a .then() may not run
on rejection; change the finalization to use .finally() so stopLoading() is
always dispatched regardless of success or failure—replace the .then(() => {
dispatch(stopLoading()); }) attached to the invoked thunk (the
)(params)(dispatch) promise) with .finally(() => { dispatch(stopLoading()); })
to ensure reliable cleanup.

};

const normalizeItems = (items) =>
items.map((item) => {
const { quantity, custom_rate, ...normalizedItem } = item;
const hasQtyFields = item.meta_fields.some(
(f) => f.class_field === "Form" && f.type_name === "Quantity"
);
const metaFields = item.meta_fields.filter(
(item) => item.current_value !== null
);

return {
...normalizedItem,
...(hasQtyFields ? {} : { quantity }),
...(custom_rate > 0 ? { custom_rate } : {}),
meta_fields: metaFields
};
});
Comment on lines +247 to +263
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Searching for custom_rate usage in form/page components ==="
rg -n "custom_rate" src/ --type=js -B 2 -A 2 | head -100

echo ""
echo "=== Looking for currency conversion utilities ==="
rg -n "amountToCents|toCents|dollarsTo|centsToDollars|currencyAmount" src/utils/ src/lib/ --type=js

echo ""
echo "=== Checking sponsor-cart-actions.js reducer line 88 for context ==="
sed -n '80,100p' src/actions/sponsor-cart-actions.js

Repository: fntechgit/summit-admin

Length of output: 5249


🏁 Script executed:

#!/bin/bash
echo "=== Finding MuiFormikPriceField implementation ==="
rg -n "const MuiFormikPriceField|function MuiFormikPriceField|export.*MuiFormikPriceField" src/ --type=js -A 10

echo ""
echo "=== Checking where normalizeItems is called ==="
rg -n "normalizeItems\(" src/actions/sponsor-cart-actions.js -B 5 -A 5

echo ""
echo "=== Finding currencyAmountFromCents definition ==="
rg -n "currencyAmountFromCents|amountFromCents" src/ --type=js -B 2 -A 3 | head -50

Repository: fntechgit/summit-admin

Length of output: 6032


🏁 Script executed:

#!/bin/bash
echo "=== Looking for conversion functions to cents ==="
rg -n "amountToCents|toCents|inCents.*true" src/ --type=js

echo ""
echo "=== Check MuiFormikPriceField full implementation for conversion logic ==="
cat -n src/components/mui/formik-inputs/mui-formik-pricefield.js

Repository: fntechgit/summit-admin

Length of output: 4094


Convert custom_rate to cents before submission.

The custom_rate field in the form stores dollars (MuiFormikPriceField without inCents prop), but the API expects cents. The reducer converts FROM cents on read, so normalizeItems must convert TO cents on write. Use amountToCents() before including custom_rate in the submission payload:

Suggested fix
...(custom_rate > 0 ? { custom_rate: amountToCents(custom_rate) } : {})

Remember to import amountToCents from "openstack-uicore-foundation/lib/utils/money".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/sponsor-cart-actions.js` around lines 247 - 263, In
normalizeItems, custom_rate is left in dollars but the API expects cents; update
the inclusion so when custom_rate > 0 you pass custom_rate:
amountToCents(custom_rate) instead of the dollar value, and add an import for
amountToCents from "openstack-uicore-foundation/lib/utils/money"; keep the
existing logic for quantity, meta_fields and hasQtyFields in the normalizeItems
function unchanged.


export const addCartForm =
(formId, addOnId, formValues) => async (dispatch, getState) => {
const { currentSummitState, currentSponsorState } = getState();
const accessToken = await getAccessTokenSafely();
const { currentSummit } = currentSummitState;
const { entity: sponsor } = currentSponsorState;

const params = {
access_token: accessToken
};

dispatch(startLoading());

const normalizedEntity = {
form_id: formId,
addon_id: addOnId,
discount_type: formValues.discount_type,
discount_amount: formValues.discount_amount,
items: normalizeItems(formValues.items)
Comment on lines 278 to 283
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize amount discounts before sending discount_value.

discount_amount comes from a dollar input, but payload sends it raw. This is inconsistent with the cents-based math used for totals and custom_rate normalization.

💡 Suggested fix
+import { FORM_DISCOUNT_OPTIONS } from "../utils/constants";
@@
     const normalizedEntity = {
       form_id: formId,
       addon_id: addOnId,
       discount_type: formValues.discount_type,
-      discount_value: formValues.discount_amount,
+      discount_value:
+        formValues.discount_type === FORM_DISCOUNT_OPTIONS.AMOUNT
+          ? amountToCents(formValues.discount_amount || 0)
+          : formValues.discount_amount,
       items: normalizeItems(formValues.items)
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/sponsor-cart-actions.js` around lines 279 - 284, The payload
currently assigns formValues.discount_amount (a dollar value) directly to
normalizedEntity.discount_value, causing inconsistency with cents-based math;
update the code that builds normalizedEntity (the normalizedEntity object in the
sponsor cart action) to convert formValues.discount_amount from dollars to
integer cents (e.g., multiply by 100 and round/parseInt) before assigning to
discount_value so it matches custom_rate/item normalization and totals
calculation; ensure the conversion is done where normalizedEntity is created so
downstream code using discount_value receives cents consistently.

};

return postRequest(
null,
createAction(FORM_CART_SAVED),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/carts/current/forms`,
normalizedEntity,
snackbarErrorHandler
)(params)(dispatch)
.then(() => {
dispatch(
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("sponsor_list.sponsor_added")
})
);
})
.finally(() => dispatch(stopLoading()));
};
5 changes: 4 additions & 1 deletion src/components/CustomTheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ const theme = createTheme({
MuiFormHelperText: {
styleOverrides: {
root: {
fontSize: ".8em"
fontSize: ".8em",
position: "absolute",
top: "100%",
marginTop: "4px"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import EditIcon from "@mui/icons-material/Edit";

import useScrollToError from "../../../hooks/useScrollToError";
import MuiFormikTextField from "../../mui/formik-inputs/mui-formik-textfield";
import SummitAddonSelect from "../../mui/formik-inputs/summit-addon-select";
import MuiFormikSummitAddonSelect from "../../mui/formik-inputs/mui-formik-summit-addon-select";

const ManageTierAddonsPopup = ({
sponsorship,
Expand Down Expand Up @@ -223,14 +223,16 @@ const ManageTierAddonsPopup = ({
</InputLabel>
{editingRow === index ? (
<Box width="100%">
<SummitAddonSelect
<MuiFormikSummitAddonSelect
name={`addons[${index}].type`}
fullWidth
placeholder={T.translate(
"edit_sponsor.placeholders.select"
)}
summitId={summitId}
margin="none"
inputProps={{
fullWidth: true,
margin: "none"
}}
/>
</Box>
) : (
Expand Down Expand Up @@ -318,15 +320,16 @@ const ManageTierAddonsPopup = ({
{T.translate("edit_sponsor.addon_type")}
</InputLabel>
<Box width="100%">
<SummitAddonSelect
<MuiFormikSummitAddonSelect
name="newAddon.type"
formik={formik}
fullWidth
placeholder={T.translate(
"edit_sponsor.placeholders.select"
)}
summitId={summitId}
margin="none"
inputProps={{
fullWidth: true,
margin: "none"
}}
/>
</Box>
</Grid2>
Expand Down
Loading