Conversation
| await ContributionAccountingCategoryRule.bulkCreate(rules, { | ||
| transaction, | ||
| updateOnDuplicate: ['order', 'enabled', 'name', 'predicates', 'AccountingCategoryId', 'updatedAt'], | ||
| }); |
There was a problem hiding this comment.
🚫 This code would let users edit rules they are not allowed to delete. When building the rules list above, we simply idDecode the ID, without loading the entry to make sure it belongs to the account.
We use richDiffDBEntries in other places to prevent this issue.
There was a problem hiding this comment.
Good catch, I've added a check and accompanying test case to validate this
| await applyContributionAccountingCategoryRules(order); | ||
|
|
There was a problem hiding this comment.
Not a requested change, but out of curiosity - have you considered adding that as a afterCreate hook on Orders, to have it in a single place and make sure we don't forget any path?
There was a problem hiding this comment.
There are only a couple of cases where we want to run this at the moment, I had a preference for doing it explicitly in such places to also take into consideration the roles and the semantics of the mutations where the host can perform remove/ignore a category on updating an order
There was a problem hiding this comment.
Given that there is an order.update above, another order.update in applyContributionAccountingCategoryRules, both without transaction and without reloading the order, it sounds like there is a risk of concurrency issue.
There was a problem hiding this comment.
I see what you mean, this is possible, I guess the particularities of the updates that happen mitigate this as there are other awaits inside the side effects.
I went ahead and refactored this section to execute the effects sequentially, I believe there should not be a need to reload the order explicitly as sequelize will update the internal order representation after the update.
| ...order.data, | ||
| valuesByRole: { | ||
| ...(order.data.valuesByRole || {}), |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| idDecode(rule.id, IDENTIFIER_TYPES.ACCOUNTING_CATEGORY_RULE), | ||
| ); | ||
|
|
||
| if (existingRule && existingRule.type !== 'CONTRIBUTION') { | ||
| throw new ValidationFailed('This mutation can only update contribution accounting category rules'); | ||
| } | ||
|
|
||
| if (existingRule && existingRule.CollectiveId !== account.id) { | ||
| throw new Forbidden('You are not authorized to update this rule'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Bug: The updateContributionAccountingCategoryRules mutation allows creating a new rule with a user-specified primary key if the provided id does not already exist.
Severity: MEDIUM
Suggested Fix
Add validation to check if a rule exists when an id is provided. If the rule does not exist, throw an error to reject the request, similar to the validation logic in the editAccountingCategories mutation.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: server/graphql/v2/mutation/AccountingCategoriesMutations.ts#L176-L188
Potential issue: In the `updateContributionAccountingCategoryRules` mutation, if a rule
`id` is provided that does not exist in the database, validation is skipped. The code
then calls `bulkCreate` with the `updateOnDuplicate` option, which causes Sequelize to
insert a new rule using the user-specified, non-existent ID as its primary key. This
bypasses the intended upsert logic and allows users to create records with arbitrary
primary keys, which could potentially interfere with the database's auto-increment
sequence.
Betree
left a comment
There was a problem hiding this comment.
All blockers resolved on my end!
| const valuesByRole = { | ||
| ...(order.data.valuesByRole || {}), | ||
| ...(accountingCategory === null ? { hostAdmin: { accountingCategory: { code: UncategorizedValue } } } : {}), | ||
| ...(accountingCategory?.id ? { hostAdmin: { accountingCategory: accountingCategory?.publicInfo } } : {}), | ||
| }; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| await AccountingCategoryRule.bulkCreate(rules, { | ||
| transaction, | ||
| updateOnDuplicate: ['order', 'enabled', 'name', 'predicates', 'AccountingCategoryId', 'updatedAt'], | ||
| }); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| resolve(rule) { | ||
| return rule.id; | ||
| }, |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| lock: Transaction.LOCK.UPDATE, | ||
| }); | ||
|
|
||
| const toDelete = existingRules.filter(rule => !rules.some(r => r.publicId === rule.publicId)); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| value: unknown, | ||
| order: Order, | ||
| ) => { | ||
| return typeof value === 'string' && value.length > 0 && order.description.includes(value as string); |
There was a problem hiding this comment.
Bug: The code calls order.description.includes() without checking if order.description is undefined, which will cause a TypeError.
Severity: MEDIUM
Suggested Fix
Add an optional chaining operator to safely handle cases where order.description is undefined. Change order.description.includes(value as string) to order.description?.includes(value as string).
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: server/lib/accounting/categorization/types.ts#L169
Potential issue: The `Order.description` field is optional and can be `undefined`.
However, when evaluating accounting category rules, the code at
`server/lib/accounting/categorization/types.ts:169` attempts to call
`order.description.includes(value)` without a null check. This will throw a `TypeError`
if an order without a description is processed. While a `try/catch` block prevents a
crash, the rule application will fail silently, preventing automatic categorization and
creating Sentry error noise.
| return; | ||
| } | ||
|
|
||
| const rules = await AccountingCategoryRule.getRulesForCollective(collective.HostCollectiveId, 'CONTRIBUTION'); |
There was a problem hiding this comment.
Bug: The code incorrectly uses collective.HostCollectiveId to fetch rules, which can be null even when a host object exists, causing silent failure.
Severity: MEDIUM
Suggested Fix
Use host.id instead of collective.HostCollectiveId when calling AccountingCategoryRule.getRulesForCollective. This ensures the correct host ID is used, as the host object is guaranteed to be available at that point.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: server/lib/accounting/categorization/contribution-rules.ts#L68
Potential issue: In `applyContributionAccountingCategoryRules`, the code fetches the
host for a collective but then uses `collective.HostCollectiveId` to retrieve the
accounting rules. The `getHostCollective()` method does not always populate
`collective.HostCollectiveId`, even when it successfully returns a `host` object. This
causes the subsequent rule lookup to query with a `null` ID, finding no rules and
causing the categorization to fail silently.
No description provided.