diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7be470370c..5c6adc23fe 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,6 +7,7 @@ - [ ] Make sure you’ve merged `main` into your branch shortly before creating the PR. (You should also be merging `main` into your branch regularly during development.) - [ ] Make sure you’ve accounted for any migrations. When you’re about to create the PR, bring up the application locally and then run `git status | grep migrations`. If there are any results, you probably need to add them to the branch for the PR. Your PR should have only **one** new migration file for each of the component apps, except in rare circumstances; you may need to delete some and re-run `python manage.py makemigrations` to reduce the number to one. (Also, unless in exceptional circumstances, your PR should not delete any migration files.) - [ ] Make sure that whatever feature you’re adding has tests that cover the feature. This includes test coverage to make sure that the previous workflow still works, if applicable. +- [ ] Make sure the full-submission.cy.js [Cypress test](https://github.com/GSA-TTS/FAC/blob/main/docs/testing.md#end-to-end-testing) passes, if applicable. - [ ] Do manual testing locally. Our tests are not good enough yet to allow us to skip this step. If that’s not applicable for some reason, check this box. - [ ] Verify that no Git surgery was necessary, or, if it was necessary at any point, repeat the testing after it’s finished. - [ ] Once a PR is merged, keep an eye on it until it’s deployed to dev, and do enough testing on dev to verify that it deployed successfully, the feature works as expected, and the happy path for the broad feature area (such as submission) still works. @@ -17,5 +18,5 @@ - [ ] Manually test out the changes locally, or check this box to verify that it wasn’t applicable in this case. - [ ] Check that the PR has appropriate tests. Look out for changes in HTML/JS/JSON Schema logic that may need to be captured in Python tests even though the logic isn’t in Python. - [ ] Verify that no Git surgery is necessary at any point (such as during a merge party), or, if it was, repeat the testing after it’s finished. - + The larger the PR, the stricter we should be about these points. diff --git a/.gitignore b/.gitignore index 1e454c6e72..3755775edc 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,7 @@ terraform/**/*.tfstate* terraform/**/*.tfvars terraform/shared/modules/egress-proxy/proxy.zip terraform/shared/modules/egress-proxy/test/client.zip + +# XLSX ignores +.~*# + diff --git a/backend/audit/fixtures/workbooks/should_fail/federal-awards/federal-awards-171944.xlsx b/backend/audit/fixtures/workbooks/should_fail/federal-awards/federal-awards-171944.xlsx new file mode 100644 index 0000000000..05738cf61a Binary files /dev/null and b/backend/audit/fixtures/workbooks/should_fail/federal-awards/federal-awards-171944.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_fail/federal-awards/has_two_passthrough_ids_one_name.xlsx b/backend/audit/fixtures/workbooks/should_fail/federal-awards/has_two_passthrough_ids_one_name.xlsx new file mode 100644 index 0000000000..908b81c081 Binary files /dev/null and b/backend/audit/fixtures/workbooks/should_fail/federal-awards/has_two_passthrough_ids_one_name.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_fail/federal-awards/has_two_passthrough_names_one_id.xlsx b/backend/audit/fixtures/workbooks/should_fail/federal-awards/has_two_passthrough_names_one_id.xlsx new file mode 100644 index 0000000000..1a33ee8ca3 Binary files /dev/null and b/backend/audit/fixtures/workbooks/should_fail/federal-awards/has_two_passthrough_names_one_id.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-amount-passed-through.xlsx b/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-amount-passed-through.xlsx new file mode 100644 index 0000000000..8129679ed5 Binary files /dev/null and b/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-amount-passed-through.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-cluster-total.xlsx b/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-cluster-total.xlsx new file mode 100644 index 0000000000..5719178b6b Binary files /dev/null and b/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-cluster-total.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-federal-program-total.xlsx b/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-federal-program-total.xlsx new file mode 100644 index 0000000000..4e8a5b16ff Binary files /dev/null and b/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-federal-program-total.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-total-amount-expended.xlsx b/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-total-amount-expended.xlsx new file mode 100644 index 0000000000..45333bee80 Binary files /dev/null and b/backend/audit/fixtures/workbooks/should_fail/federal-awards/incorrect-total-amount-expended.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_fail/federal-awards/invalid-loan-balance.xlsx b/backend/audit/fixtures/workbooks/should_fail/federal-awards/invalid-loan-balance.xlsx new file mode 100644 index 0000000000..dfe3ba9771 Binary files /dev/null and b/backend/audit/fixtures/workbooks/should_fail/federal-awards/invalid-loan-balance.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_fail/federal-awards/missing-additional-award-identification.xlsx b/backend/audit/fixtures/workbooks/should_fail/federal-awards/missing-additional-award-identification.xlsx new file mode 100644 index 0000000000..dcfd50d822 Binary files /dev/null and b/backend/audit/fixtures/workbooks/should_fail/federal-awards/missing-additional-award-identification.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_pass/100010-22/federal-awards-100010.xlsx b/backend/audit/fixtures/workbooks/should_pass/100010-22/federal-awards-100010.xlsx index 981398cafe..df59c75406 100644 Binary files a/backend/audit/fixtures/workbooks/should_pass/100010-22/federal-awards-100010.xlsx and b/backend/audit/fixtures/workbooks/should_pass/100010-22/federal-awards-100010.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_pass/134732-21/federal-awards-134732.xlsx b/backend/audit/fixtures/workbooks/should_pass/134732-21/federal-awards-134732.xlsx index 7c3f1e8fcf..b1d50e36d4 100644 Binary files a/backend/audit/fixtures/workbooks/should_pass/134732-21/federal-awards-134732.xlsx and b/backend/audit/fixtures/workbooks/should_pass/134732-21/federal-awards-134732.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_pass/147110-22/federal-awards-147110.xlsx b/backend/audit/fixtures/workbooks/should_pass/147110-22/federal-awards-147110.xlsx index 2842f30225..09bd30dccf 100644 Binary files a/backend/audit/fixtures/workbooks/should_pass/147110-22/federal-awards-147110.xlsx and b/backend/audit/fixtures/workbooks/should_pass/147110-22/federal-awards-147110.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_pass/171944-22/federal-awards-171944.xlsx b/backend/audit/fixtures/workbooks/should_pass/171944-22/federal-awards-171944.xlsx deleted file mode 100644 index 964629c540..0000000000 Binary files a/backend/audit/fixtures/workbooks/should_pass/171944-22/federal-awards-171944.xlsx and /dev/null differ diff --git a/backend/audit/fixtures/workbooks/should_pass/181744-22/federal-awards-181744.xlsx b/backend/audit/fixtures/workbooks/should_pass/181744-22/federal-awards-181744.xlsx index 5789944fca..847bb4822a 100644 Binary files a/backend/audit/fixtures/workbooks/should_pass/181744-22/federal-awards-181744.xlsx and b/backend/audit/fixtures/workbooks/should_pass/181744-22/federal-awards-181744.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_pass/182926-22/federal-awards-182926.xlsx b/backend/audit/fixtures/workbooks/should_pass/182926-22/federal-awards-182926.xlsx index 640f03bff0..2fad1d0640 100644 Binary files a/backend/audit/fixtures/workbooks/should_pass/182926-22/federal-awards-182926.xlsx and b/backend/audit/fixtures/workbooks/should_pass/182926-22/federal-awards-182926.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_pass/191734-21/federal-awards-191734.xlsx b/backend/audit/fixtures/workbooks/should_pass/191734-21/federal-awards-191734.xlsx index e95848b0e7..1469ae30bf 100644 Binary files a/backend/audit/fixtures/workbooks/should_pass/191734-21/federal-awards-191734.xlsx and b/backend/audit/fixtures/workbooks/should_pass/191734-21/federal-awards-191734.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_pass/191734-22/federal-awards-191734.xlsx b/backend/audit/fixtures/workbooks/should_pass/191734-22/federal-awards-191734.xlsx index a24d08ee40..e277118698 100644 Binary files a/backend/audit/fixtures/workbooks/should_pass/191734-22/federal-awards-191734.xlsx and b/backend/audit/fixtures/workbooks/should_pass/191734-22/federal-awards-191734.xlsx differ diff --git a/backend/audit/fixtures/workbooks/should_pass/219107-21/federal-awards-219107.xlsx b/backend/audit/fixtures/workbooks/should_pass/219107-21/federal-awards-219107.xlsx index e728e744db..6a2a5ba5b9 100644 Binary files a/backend/audit/fixtures/workbooks/should_pass/219107-21/federal-awards-219107.xlsx and b/backend/audit/fixtures/workbooks/should_pass/219107-21/federal-awards-219107.xlsx differ diff --git a/backend/audit/intakelib/checks/check_additional_award_identification_present.py b/backend/audit/intakelib/checks/check_additional_award_identification_present.py new file mode 100644 index 0000000000..002fe5724c --- /dev/null +++ b/backend/audit/intakelib/checks/check_additional_award_identification_present.py @@ -0,0 +1,34 @@ +import logging +import re +from audit.intakelib.intermediate_representation import ( + get_range_values_by_name, + get_range_by_name, +) +from .check_aln_three_digit_extension_pattern import ( + REGEX_RD_EXTENSION, + REGEX_U_EXTENSION, +) +from .util import get_message, build_cell_error_tuple + +logger = logging.getLogger(__name__) + + +# DESCRIPTION +# Additional award identification should be present if the ALN three digit extension is RD#, or U## +def additional_award_identification(ir): + extension = get_range_values_by_name(ir, "three_digit_extension") + additional = get_range_values_by_name(ir, "additional_award_identification") + errors = [] + patterns = [REGEX_RD_EXTENSION, REGEX_U_EXTENSION] + for index, (ext, add) in enumerate(zip(extension, additional)): + if any(re.match(pattern, ext) for pattern in patterns) and not add: + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "additional_award_identification"), + index, + get_message("check_additional_award_identification_present"), + ) + ) + + return errors diff --git a/backend/audit/intakelib/checks/check_aln_three_digit_extension_pattern.py b/backend/audit/intakelib/checks/check_aln_three_digit_extension_pattern.py new file mode 100644 index 0000000000..1874185df6 --- /dev/null +++ b/backend/audit/intakelib/checks/check_aln_three_digit_extension_pattern.py @@ -0,0 +1,45 @@ +import logging +import re +from audit.intakelib.intermediate_representation import ( + get_range_values_by_name, + get_range_by_name, +) +from .util import get_message, build_cell_error_tuple + +logger = logging.getLogger(__name__) + +# A version of these regexes also exists in Base.libsonnet +REGEX_RD_EXTENSION = r"^RD[0-9]?$" +REGEX_THREE_DIGIT_EXTENSION = r"^[0-9]{3}[A-Za-z]{0,1}$" +REGEX_U_EXTENSION = r"^U[0-9]{2}$" + + +# DESCRIPTION +# The three digit extension should follow one of these formats: ###, RD#, or U##, where # represents a number +def aln_three_digit_extension(ir): + extension = get_range_values_by_name(ir, "three_digit_extension") + errors = [] + # Define regex patterns + patterns = [REGEX_RD_EXTENSION, REGEX_THREE_DIGIT_EXTENSION, REGEX_U_EXTENSION] + for index, ext in enumerate(extension): + # Check if ext is None or does not match any of the regex patterns + if not ext: + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "three_digit_extension"), + index, + get_message("check_aln_three_digit_extension_missing"), + ) + ) + elif not any(re.match(pattern, ext) for pattern in patterns): + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "three_digit_extension"), + index, + get_message("check_aln_three_digit_extension_invalid"), + ) + ) + + return errors diff --git a/backend/audit/intakelib/checks/check_cardinality_of_passthrough_names_and_ids.py b/backend/audit/intakelib/checks/check_cardinality_of_passthrough_names_and_ids.py new file mode 100644 index 0000000000..cef39db826 --- /dev/null +++ b/backend/audit/intakelib/checks/check_cardinality_of_passthrough_names_and_ids.py @@ -0,0 +1,47 @@ +import logging +from audit.intakelib.intermediate_representation import ( + get_range_by_name, +) +from .util import get_message, build_cell_error_tuple + +logger = logging.getLogger(__name__) + + +# DESCRIPTION +# If users provide a `|` in the passthrough id number field, +# we expect multiple names in the name field, too. +def cardinality_of_passthrough_names_and_ids(ir): + passthrough_name = get_range_by_name(ir, "passthrough_name") + passthrough_identifying_number = get_range_by_name( + ir, "passthrough_identifying_number" + ) + + errors = [] + + for index, (pass_name, pass_id) in enumerate( + zip(passthrough_name["values"], passthrough_identifying_number["values"]) + ): + if (pass_name and pass_id) and ("|" in pass_name or "|" in pass_id): + names = pass_name.split("|") + ids = pass_id.split("|") + print(names, ids) + if len(names) != len(ids): + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "passthrough_name"), + index, + ( + get_message( + "check_cardinality_of_passthrough_names_and_ids" + ).format( + len(names), + "s" if len(names) > 1 else "", + len(ids), + "s" if len(ids) > 1 else "", + ) + ), + ) + ) + + return errors diff --git a/backend/audit/intakelib/checks/check_cluster_total.py b/backend/audit/intakelib/checks/check_cluster_total.py new file mode 100644 index 0000000000..3bd6fecbc2 --- /dev/null +++ b/backend/audit/intakelib/checks/check_cluster_total.py @@ -0,0 +1,75 @@ +import logging +from audit.intakelib.intermediate_representation import ( + get_range_values_by_name, + get_range_by_name, +) +from .util import get_message, build_cell_error_tuple + +logger = logging.getLogger(__name__) + +STATE_CLUSTER = "STATE CLUSTER" +OTHER_CLUSTER = "OTHER CLUSTER NOT LISTED ABOVE" +NOT_APPLICABLE = "N/A" + + +# DESCRIPTION +# The sum of the amount_expended for a given cluster should equal the corresponding cluster_total +# K{0}=IF(G{0}="OTHER CLUSTER NOT LISTED ABOVE",SUMIFS(amount_expended,uniform_other_cluster_name,X{0}),IF(AND(OR(G{0}="N/A",G{0}=""),H{0}=""),0,IF(G{0}="STATE CLUSTER",SUMIFS(amount_expended,uniform_state_cluster_name,W{0}),SUMIFS(amount_expended,cluster_name,G{0})))) +def cluster_total_is_correct(ir): + uniform_other_cluster_name = get_range_values_by_name( + ir, "uniform_other_cluster_name" + ) + uniform_state_cluster_name = get_range_values_by_name( + ir, "uniform_state_cluster_name" + ) + state_cluster_name = get_range_values_by_name(ir, "state_cluster_name") + cluster_name = get_range_values_by_name(ir, "cluster_name") + cluster_total = get_range_values_by_name(ir, "cluster_total") + amount_expended = get_range_values_by_name(ir, "amount_expended") + + errors = [] + + # Validating each cluster_total + for idx, name in enumerate(cluster_name): + # Based on the formula's conditions + if name == OTHER_CLUSTER: + expected_value = sum( + [ + amount + for k, amount in zip(uniform_other_cluster_name, amount_expended) + if k == uniform_other_cluster_name[idx] + ] + ) + elif (name == NOT_APPLICABLE or not name) and not state_cluster_name[idx]: + expected_value = 0 + elif name == STATE_CLUSTER: + expected_value = sum( + [ + amount + for k, amount in zip(uniform_state_cluster_name, amount_expended) + if k == uniform_state_cluster_name[idx] + ] + ) + elif name: + expected_value = sum( + [ + amount + for k, amount in zip(cluster_name, amount_expended) + if k == name + ] + ) + + # Check if the calculated value matches the provided one + if expected_value != cluster_total[idx]: + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "cluster_total"), + idx, + get_message("check_cluster_total").format( + cluster_total[idx], expected_value + ), + ) + ) + + return errors diff --git a/backend/audit/intakelib/checks/check_federal_award_passed_passed_through_optional.py b/backend/audit/intakelib/checks/check_federal_award_passed_passed_through_optional.py new file mode 100644 index 0000000000..d28e043d53 --- /dev/null +++ b/backend/audit/intakelib/checks/check_federal_award_passed_passed_through_optional.py @@ -0,0 +1,42 @@ +import logging +from audit.intakelib.intermediate_representation import ( + get_range_values_by_name, + get_range_by_name, +) +from .util import get_message, build_cell_error_tuple + +logger = logging.getLogger(__name__) + + +# DESCRIPTION +# Amount passed through must be present if "Y" is selected for "Federal Award Passed Through" +def federal_award_amount_passed_through_optional(ir): + is_passed_values = get_range_values_by_name(ir, "is_passed") + subrecipient_amount_values = get_range_values_by_name(ir, "subrecipient_amount") + errors = [] + + for index, (is_passed, subrecipient_amount) in enumerate( + zip(is_passed_values, subrecipient_amount_values) + ): + if is_passed == "Y" and subrecipient_amount is None: + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "subrecipient_amount"), + index, + get_message("check_federal_award_amount_passed_through_required"), + ) + ) + elif is_passed == "N" and subrecipient_amount is not None: + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "subrecipient_amount"), + index, + get_message( + "check_federal_award_amount_passed_through_not_allowed" + ), + ) + ) + + return errors diff --git a/backend/audit/intakelib/checks/check_federal_program_total.py b/backend/audit/intakelib/checks/check_federal_program_total.py new file mode 100644 index 0000000000..d7389f418f --- /dev/null +++ b/backend/audit/intakelib/checks/check_federal_program_total.py @@ -0,0 +1,39 @@ +import logging +from audit.intakelib.intermediate_representation import ( + get_range_values_by_name, + get_range_by_name, +) +from .util import get_message, build_cell_error_tuple + +logger = logging.getLogger(__name__) + + +# DESCRIPTION +# The sum of the amount_expended for a given cfda_key should equal the corresponding federal_program_total +# J{0}=SUMIFS(amount_expended,cfda_key,V{0})) +def federal_program_total_is_correct(ir): + federal_program_total = get_range_values_by_name(ir, "federal_program_total") + cfda_key = get_range_values_by_name(ir, "cfda_key") + amount_expended = get_range_values_by_name(ir, "amount_expended") + + errors = [] + + # Validating each federal_program_total + for idx, key in enumerate(cfda_key): + # Compute the sum for current cfda_key + computed_sum = sum( + [amount for k, amount in zip(cfda_key, amount_expended) if k == key] + ) + if computed_sum != federal_program_total[idx]: + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "federal_program_total"), + idx, + get_message("check_federal_program_total").format( + federal_program_total[idx], computed_sum + ), + ) + ) + + return errors diff --git a/backend/audit/intakelib/checks/check_loan_balance.py b/backend/audit/intakelib/checks/check_loan_balance.py new file mode 100644 index 0000000000..ace3bd0b95 --- /dev/null +++ b/backend/audit/intakelib/checks/check_loan_balance.py @@ -0,0 +1,40 @@ +import logging +from audit.intakelib.intermediate_representation import ( + get_range_values_by_name, + get_range_by_name, +) +from .util import get_message, build_cell_error_tuple + +logger = logging.getLogger(__name__) + + +# DESCRIPTION +# This makes sure that the loan guarantee is either a numerical value or N/A or an empty string. +def loan_balance(ir): + loan_balance_at_period_end = get_range_values_by_name( + ir, "loan_balance_at_audit_period_end" + ) + + errors = [] + for index, balance in enumerate(loan_balance_at_period_end): + # Check if balance is not a number, empty string, or "N/A" + if not (balance in ["N/A", "", None] or _is_int(balance)): + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "loan_balance_at_audit_period_end"), + index, + get_message("check_loan_balance").format(balance), + ) + ) + + return errors + + +# check if the given string can be converted to an int +def _is_int(s): + try: + value = int(s) + return value >= 0 + except ValueError: + return False diff --git a/backend/audit/intakelib/checks/check_total_amount_expended.py b/backend/audit/intakelib/checks/check_total_amount_expended.py new file mode 100644 index 0000000000..e9a041bed9 --- /dev/null +++ b/backend/audit/intakelib/checks/check_total_amount_expended.py @@ -0,0 +1,34 @@ +import logging +from audit.intakelib.intermediate_representation import ( + get_range_values_by_name, + get_range_by_name, +) +from .util import get_message, build_cell_error_tuple + +logger = logging.getLogger(__name__) + + +# DESCRIPTION +# The sum of the amount_expended should equal the total_amount_expended +# B5=SUM(Form!F$2:F$5000) +def total_amount_expended_is_correct(ir): + total_amount_expended_value = get_range_values_by_name(ir, "total_amount_expended") + amount_expended_values = get_range_values_by_name(ir, "amount_expended") + + errors = [] + + # Validating total_amount_expended + computed_sum = sum(amount_expended_values) + if computed_sum != total_amount_expended_value[0]: + errors.append( + build_cell_error_tuple( + ir, + get_range_by_name(ir, "total_amount_expended"), + 0, + get_message("check_total_amount_expended").format( + total_amount_expended_value[0], computed_sum + ), + ) + ) + + return errors diff --git a/backend/audit/intakelib/checks/error_messages.py b/backend/audit/intakelib/checks/error_messages.py index df19414a0a..a5c0015765 100644 --- a/backend/audit/intakelib/checks/error_messages.py +++ b/backend/audit/intakelib/checks/error_messages.py @@ -26,4 +26,14 @@ "check_passthrough_name_when_no_direct_n_and_empty_number": "When the award is direct, passthrough number must be empty", "check_findings_grid_validation": "The combination of findings {} is not a valid combination under Uniform Guidance", "check_eins_are_not_empty": "EIN cannot be empty", + "check_aln_three_digit_extension_missing": "Missing ALN (CFDA) three digit extension", + "check_aln_three_digit_extension_invalid": "The three digit extension should follow one of these formats: ###, RD#, or U##, where # represents a number", + "check_additional_award_identification_present": "Missing additional award identification", + "check_federal_program_total": "Federal program total is {}, but should be {}", + "check_cluster_total": "This cluster total is {}, but should be {}", + "check_total_amount_expended": "Total amount expended is {}, but should be {}", + "check_federal_award_amount_passed_through_required": "When Federal Award Passed Through is Y, Amount Passed Through cannot be empty", + "check_federal_award_amount_passed_through_not_allowed": "When Federal Award Passed Through is N, Amount Passed Through must be empty", + "check_loan_balance": "The loan balance is currently set to {}. It should either be a positive number, N/A, or left empty.", + "check_cardinality_of_passthrough_names_and_ids": "You used a | (bar character) to indicate multiple passthrough names and IDs; you must provide equal numbers of names and IDs. You provided {} name{} and {} ID{}", } diff --git a/backend/audit/intakelib/checks/runners.py b/backend/audit/intakelib/checks/runners.py index 4f2f31dfc1..88d4f31f2c 100644 --- a/backend/audit/intakelib/checks/runners.py +++ b/backend/audit/intakelib/checks/runners.py @@ -19,6 +19,7 @@ from .check_direct_award_is_not_blank import direct_award_is_not_blank from .check_passthrough_name_when_no_direct import passthrough_name_when_no_direct from .check_loan_guarantee import loan_guarantee +from .check_loan_balance import loan_balance from .check_no_major_program_no_type import no_major_program_no_type from .check_missing_award_numbers import missing_award_numbers from .check_all_unique_award_numbers import all_unique_award_numbers @@ -28,6 +29,19 @@ from .check_federal_award_passed_always_present import ( federal_award_passed_always_present, ) +from .check_aln_three_digit_extension_pattern import aln_three_digit_extension +from .check_additional_award_identification_present import ( + additional_award_identification, +) +from .check_federal_program_total import federal_program_total_is_correct +from .check_cluster_total import cluster_total_is_correct +from .check_total_amount_expended import total_amount_expended_is_correct +from .check_federal_award_passed_passed_through_optional import ( + federal_award_amount_passed_through_optional, +) +from .check_cardinality_of_passthrough_names_and_ids import ( + cardinality_of_passthrough_names_and_ids, +) ############ # Audit findings checks @@ -49,14 +63,22 @@ num_findings_always_present, cluster_name_always_present, federal_award_passed_always_present, + federal_award_amount_passed_through_optional, state_cluster_names, other_cluster_names, direct_award_is_not_blank, passthrough_name_when_no_direct, + loan_balance, loan_guarantee, no_major_program_no_type, all_unique_award_numbers, sequential_award_numbers, + aln_three_digit_extension, + additional_award_identification, + federal_program_total_is_correct, + cluster_total_is_correct, + total_amount_expended_is_correct, + cardinality_of_passthrough_names_and_ids, ] notes_to_sefa_checks = general_checks + [ diff --git a/backend/audit/intakelib/mapping_federal_awards.py b/backend/audit/intakelib/mapping_federal_awards.py index 2c007d2fbb..5ed928d288 100644 --- a/backend/audit/intakelib/mapping_federal_awards.py +++ b/backend/audit/intakelib/mapping_federal_awards.py @@ -30,6 +30,7 @@ from .mapping_meta import meta_mapping from .checks import run_all_general_checks, run_all_federal_awards_checks +from .transforms import run_all_federal_awards_transforms logger = logging.getLogger(__name__) @@ -48,10 +49,17 @@ def extract_federal_awards(file): template["title_row"], ) + # ir = extract_workbook_as_ir(file) + # run_all_general_checks(ir, FORM_SECTIONS.FEDERAL_AWARDS_EXPENDED) + # run_all_federal_awards_checks(ir) + # result = _extract_generic_data(ir, params) + ir = extract_workbook_as_ir(file) run_all_general_checks(ir, FORM_SECTIONS.FEDERAL_AWARDS_EXPENDED) - run_all_federal_awards_checks(ir) - result = _extract_generic_data(ir, params) + new_ir = run_all_federal_awards_transforms(ir) + run_all_federal_awards_checks(new_ir) + result = _extract_generic_data(new_ir, params) + return result diff --git a/backend/audit/intakelib/transforms/__init__.py b/backend/audit/intakelib/transforms/__init__.py index 4813e5a22e..836a75a9a6 100644 --- a/backend/audit/intakelib/transforms/__init__.py +++ b/backend/audit/intakelib/transforms/__init__.py @@ -2,4 +2,5 @@ from .runners import ( run_all_notes_to_sefa_transforms, run_all_additional_eins_transforms, + run_all_federal_awards_transforms, ) diff --git a/backend/audit/intakelib/transforms/runners.py b/backend/audit/intakelib/transforms/runners.py index 204b2f683e..1da6994115 100644 --- a/backend/audit/intakelib/transforms/runners.py +++ b/backend/audit/intakelib/transforms/runners.py @@ -15,6 +15,11 @@ from .xform_eins_need_to_be_strings import eins_need_to_be_strings +from .xform_all_alns_need_to_be_strings import all_alns_need_to_be_strings +from .xform_all_passthrough_id_need_to_be_strings import ( + all_passthrough_id_need_to_be_strings, +) + logger = logging.getLogger(__name__) @@ -33,6 +38,10 @@ def run_all_additional_eins_transforms(ir): return run_all_transforms(ir, additional_eins_transforms) +def run_all_federal_awards_transforms(ir): + return run_all_transforms(ir, federal_awards_transforms) + + general_transforms = [no_op] notes_to_sefa_transforms = general_transforms + [ @@ -43,3 +52,8 @@ def run_all_additional_eins_transforms(ir): additional_eins_transforms = general_transforms + [ eins_need_to_be_strings, ] + +federal_awards_transforms = general_transforms + [ + all_alns_need_to_be_strings, + all_passthrough_id_need_to_be_strings, +] diff --git a/backend/audit/intakelib/transforms/xform_all_alns_need_to_be_strings.py b/backend/audit/intakelib/transforms/xform_all_alns_need_to_be_strings.py new file mode 100644 index 0000000000..a60b907e60 --- /dev/null +++ b/backend/audit/intakelib/transforms/xform_all_alns_need_to_be_strings.py @@ -0,0 +1,19 @@ +import logging +from audit.intakelib.intermediate_representation import ( + get_range_by_name, + replace_range_by_name, +) + +logger = logging.getLogger(__name__) + + +def all_alns_need_to_be_strings(ir): + agencies = get_range_by_name(ir, "federal_agency_prefix") + new_values = list(map(lambda v: str(v), agencies["values"])) + new_ir = replace_range_by_name(ir, "federal_agency_prefix", new_values) + + extensions = get_range_by_name(ir, "three_digit_extension") + new_values = list(map(lambda v: str(v), extensions["values"])) + new_ir = replace_range_by_name(ir, "three_digit_extension", new_values) + + return new_ir diff --git a/backend/audit/intakelib/transforms/xform_all_passthrough_id_need_to_be_strings.py b/backend/audit/intakelib/transforms/xform_all_passthrough_id_need_to_be_strings.py new file mode 100644 index 0000000000..165bdf3667 --- /dev/null +++ b/backend/audit/intakelib/transforms/xform_all_passthrough_id_need_to_be_strings.py @@ -0,0 +1,15 @@ +import logging +from audit.intakelib.intermediate_representation import ( + get_range_by_name, + replace_range_by_name, +) + +logger = logging.getLogger(__name__) + + +def all_passthrough_id_need_to_be_strings(ir): + passthrough_ids = get_range_by_name(ir, "passthrough_identifying_number") + new_values = [str(v) if v is not None else None for v in passthrough_ids["values"]] + new_ir = replace_range_by_name(ir, "passthrough_identifying_number", new_values) + + return new_ir diff --git a/backend/audit/templates/audit/my_submissions.html b/backend/audit/templates/audit/my_submissions.html index 3c3e5df23d..25dafbbc32 100644 --- a/backend/audit/templates/audit/my_submissions.html +++ b/backend/audit/templates/audit/my_submissions.html @@ -53,7 +53,16 @@
Status | @@ -72,7 +81,13 @@||
---|---|---|
- {{ item.submission_status }} + + {% if item.submission_status == "Disseminated" %} + Accepted + {% else %} + {{ item.submission_status }} + {% endif %} + | {{ item.auditee_name }} | {{ item.report_id }} | diff --git a/backend/cypress/e2e/full-submission.cy.js b/backend/cypress/e2e/full-submission.cy.js index 08c215b98b..80eed45668 100644 --- a/backend/cypress/e2e/full-submission.cy.js +++ b/backend/cypress/e2e/full-submission.cy.js @@ -1,164 +1,15 @@ -import { testCrossValidation } from '../support/cross-validation.js'; -import { testLoginGovLogin } from '../support/login-gov.js'; -import { testLogoutGov } from '../support/logout-gov.js'; -import { testValidAccess } from '../support/check-access.js'; -import { testValidEligibility } from '../support/check-eligibility.js'; -import { testValidAuditeeInfo } from '../support/auditee-info.js'; -import { testValidGeneralInfo } from '../support/general-info.js'; -import { testAuditInformationForm } from '../support/audit-info-form.js'; -import { testPdfAuditReport } from '../support/report-pdf.js'; -import { testAuditorCertification } from '../support/auditor-certification.js'; -import { testAuditeeCertification } from '../support/auditee-certification.js'; -import { testReportIdFound, testReportIdNotFound } from '../support/dissemination-table.js'; -import { testTribalAuditPublic, testTribalAuditPrivate } from '../support/tribal-audit-form.js'; +import { testFullSubmission } from '../support/full-submission.js'; -import { - testWorkbookFederalAwards, - testWorkbookNotesToSEFA, - testWorkbookFindingsUniformGuidance, - testWorkbookFindingsText, - testWorkbookCorrectiveActionPlan, - testWorkbookAdditionalUEIs, - testWorkbookSecondaryAuditors, - testWorkbookAdditionalEINs -} from '../support/workbook-uploads.js'; - -const LOGIN_TEST_EMAIL_AUDITEE = Cypress.env('LOGIN_TEST_EMAIL_AUDITEE'); -const LOGIN_TEST_PASSWORD_AUDITEE = Cypress.env('LOGIN_TEST_PASSWORD_AUDITEE'); -const LOGIN_TEST_OTP_SECRET_AUDITEE = Cypress.env('LOGIN_TEST_OTP_SECRET_AUDITEE'); - -describe('Full audit submission', () => { - before(() => { - cy.visit('/'); +describe('Full audit submissions', () => { + it('Non-tribal, public', () => { + testFullSubmission(false, true); }); - it('Completes a full submission', () => { - cy.url().should('include', '/'); - - // Logs in with Login.gov' - testLoginGovLogin(); - - // Moves on to the eligibility screen - // check the terms and conditions link and click "Accept and start..." - // - // this click actually goes to the "terms and conditions" link which - // brings up a modal - cy.get('label[for=check-start-new-submission]').click(); - cy.get('.usa-button').contains('Accept and start').click(); - cy.url().should('match', /\/report_submission\/eligibility\/$/); - - // Completes the eligibility screen - testValidEligibility(); - - // Now the auditee info screen - testValidAuditeeInfo(); - - // Now the accessandsubmission screen - testValidAccess(); - - // Report should not yet be in the dissemination table - cy.url().then(url => { - const reportId = url.split('/').pop(); - testReportIdNotFound(reportId); - }); - - // Fill out the general info form - testValidGeneralInfo(); - - // Upload all the workbooks. Don't intercept the uploads, which means a file will make it into the DB. - cy.get(".usa-link").contains("Federal Awards").click(); - testWorkbookFederalAwards(false); - - cy.get(".usa-link").contains("Notes to SEFA").click(); - testWorkbookNotesToSEFA(false); - - cy.get(".usa-link").contains("Audit report PDF").click(); - testPdfAuditReport(false); - - cy.get(".usa-link").contains("Federal Awards Audit Findings").click(); - testWorkbookFindingsUniformGuidance(false); - - cy.get(".usa-link").contains("Federal Awards Audit Findings Text").click(); - testWorkbookFindingsText(false); - - cy.get(".usa-link").contains("Corrective Action Plan").click(); - testWorkbookCorrectiveActionPlan(false); - - cy.get(".usa-link").contains("Additional UEIs").click(); - testWorkbookAdditionalUEIs(false); - - cy.get(".usa-link").contains("Secondary Auditors").click(); - testWorkbookSecondaryAuditors(false); - - cy.get(".usa-link").contains("Additional EINs").click(); - testWorkbookAdditionalEINs(false); - - cy.url().then(url => { - const reportId = url.split('/').pop(); - - // Login as Auditee - testLogoutGov(); - testLoginGovLogin( - LOGIN_TEST_EMAIL_AUDITEE, - LOGIN_TEST_PASSWORD_AUDITEE, - LOGIN_TEST_OTP_SECRET_AUDITEE - ); - cy.visit(`/audit/submission-progress/${reportId}`); - - // complete the tribal audit form as auditee - opt private - cy.get(".usa-link").contains("Tribal data release").click(); - testTribalAuditPrivate(); - - // Login as Auditor - testLogoutGov(); - testLoginGovLogin(); - cy.visit(`/audit/submission-progress/${reportId}`); - }) - - // Complete the audit information form - cy.get(".usa-link").contains("Audit Information form").click(); - testAuditInformationForm(); - - cy.get(".usa-link").contains("Pre-submission validation").click(); - testCrossValidation(); - - // Auditor certification - cy.get(".usa-link").contains("Auditor Certification").click(); - testAuditorCertification(); - - // Grab the report ID from the URL - cy.url().then(url => { - const reportId = url.split('/').pop(); - - testLogoutGov(); - - // Login as Auditee - testLoginGovLogin( - LOGIN_TEST_EMAIL_AUDITEE, - LOGIN_TEST_PASSWORD_AUDITEE, - LOGIN_TEST_OTP_SECRET_AUDITEE - ); - - cy.visit(`/audit/submission-progress/${reportId}`); - - // Auditee certification - cy.get(".usa-link").contains("Auditee Certification").click(); - testAuditeeCertification(); - - // Submit - cy.get(".usa-link").contains("Submit to the FAC for processing").click(); - cy.url().should('match', /\/audit\/submission\/[0-9]{4}-[0-9]{2}-GSAFAC-[0-9]{10}/); - cy.get('#continue').click(); - cy.url().should('match', /\/audit\//); - - // The report ID should be found in the Completed Audits table - cy.get('.usa-table').contains( - 'caption', - 'The audits listed below have been submitted to the FAC for processing and may not be edited.', - ).siblings().contains('td', reportId); + it('Tribal, public', () => { + testFullSubmission(true, true); + }); - // The Report should not be in the dissemination table - testReportIdNotFound(reportId); - }); + it('Tribal, non-public', () => { + testFullSubmission(true, false); }); }); diff --git a/backend/cypress/support/check-eligibility.js b/backend/cypress/support/check-eligibility.js index e941c20b76..f41d500213 100644 --- a/backend/cypress/support/check-eligibility.js +++ b/backend/cypress/support/check-eligibility.js @@ -1,13 +1,13 @@ // Resusable components for the "Check Eligibility" pre-screener -export function selectValidEntries() { - cy.get('label[for=entity-tribe]').click(); +export function selectValidEntries(isTribal) { + cy.get(`label[for=entity-${isTribal ? 'tribe' : 'state'}]`).click(); cy.get('label[for=spend-yes]').click(); cy.get('label[for=us-yes]').click(); } -export function testValidEligibility() { - selectValidEntries(); +export function testValidEligibility(isTribal) { + selectValidEntries(isTribal); cy.get('.usa-button').contains('Continue').click(); cy.url().should('match', /\/report_submission\/auditeeinfo\/$/); diff --git a/backend/cypress/support/full-submission.js b/backend/cypress/support/full-submission.js new file mode 100644 index 0000000000..7d20c69d4c --- /dev/null +++ b/backend/cypress/support/full-submission.js @@ -0,0 +1,170 @@ +import { testCrossValidation } from './cross-validation.js'; +import { testLoginGovLogin } from './login-gov.js'; +import { testLogoutGov } from './logout-gov.js'; +import { testValidAccess } from './check-access.js'; +import { testValidEligibility } from './check-eligibility.js'; +import { testValidAuditeeInfo } from './auditee-info.js'; +import { testValidGeneralInfo } from './general-info.js'; +import { testAuditInformationForm } from './audit-info-form.js'; +import { testPdfAuditReport } from './report-pdf.js'; +import { testAuditorCertification } from './auditor-certification.js'; +import { testAuditeeCertification } from './auditee-certification.js'; +import { testReportIdFound, testReportIdNotFound } from './dissemination-table.js'; +import { testTribalAuditPublic, testTribalAuditPrivate } from './tribal-audit-form.js'; + +import { + testWorkbookFederalAwards, + testWorkbookNotesToSEFA, + testWorkbookFindingsUniformGuidance, + testWorkbookFindingsText, + testWorkbookCorrectiveActionPlan, + testWorkbookAdditionalUEIs, + testWorkbookSecondaryAuditors, + testWorkbookAdditionalEINs +} from './workbook-uploads.js'; + +const LOGIN_TEST_EMAIL_AUDITEE = Cypress.env('LOGIN_TEST_EMAIL_AUDITEE'); +const LOGIN_TEST_PASSWORD_AUDITEE = Cypress.env('LOGIN_TEST_PASSWORD_AUDITEE'); +const LOGIN_TEST_OTP_SECRET_AUDITEE = Cypress.env('LOGIN_TEST_OTP_SECRET_AUDITEE'); + +export function testFullSubmission(isTribal, isPublic) { + cy.visit('/'); + cy.url().should('include', '/'); + + // Logs in with Login.gov' + testLoginGovLogin(); + + // Moves on to the eligibility screen + // check the terms and conditions link and click "Accept and start..." + // + // this click actually goes to the "terms and conditions" link which + // brings up a modal + cy.get('label[for=check-start-new-submission]').click(); + cy.get('.usa-button').contains('Accept and start').click(); + cy.url().should('match', /\/report_submission\/eligibility\/$/); + + // Completes the eligibility screen + testValidEligibility(isTribal); + + // Now the auditee info screen + testValidAuditeeInfo(); + + // Now the accessandsubmission screen + testValidAccess(); + + // Report should not yet be in the dissemination table + cy.url().then(url => { + const reportId = url.split('/').pop(); + testReportIdNotFound(reportId); + }); + + // Fill out the general info form + testValidGeneralInfo(); + + // Upload all the workbooks. Don't intercept the uploads, which means a file will make it into the DB. + cy.get(".usa-link").contains("Federal Awards").click(); + testWorkbookFederalAwards(false); + + cy.get(".usa-link").contains("Notes to SEFA").click(); + testWorkbookNotesToSEFA(false); + + cy.get(".usa-link").contains("Audit report PDF").click(); + testPdfAuditReport(false); + + cy.get(".usa-link").contains("Federal Awards Audit Findings").click(); + testWorkbookFindingsUniformGuidance(false); + + cy.get(".usa-link").contains("Federal Awards Audit Findings Text").click(); + testWorkbookFindingsText(false); + + cy.get(".usa-link").contains("Corrective Action Plan").click(); + testWorkbookCorrectiveActionPlan(false); + + cy.get(".usa-link").contains("Additional UEIs").click(); + testWorkbookAdditionalUEIs(false); + + cy.get(".usa-link").contains("Secondary Auditors").click(); + testWorkbookSecondaryAuditors(false); + + cy.get(".usa-link").contains("Additional EINs").click(); + testWorkbookAdditionalEINs(false); + + if (isTribal) { + cy.url().then(url => { + const reportId = url.split('/').pop(); + + // Complete the tribal audit form as auditee - opt private + testLogoutGov(); + testLoginGovLogin( + LOGIN_TEST_EMAIL_AUDITEE, + LOGIN_TEST_PASSWORD_AUDITEE, + LOGIN_TEST_OTP_SECRET_AUDITEE + ); + cy.visit(`/audit/submission-progress/${reportId}`); + cy.get(".usa-link").contains("Tribal data release").click(); + + if (isPublic) { + testTribalAuditPublic(); + } else { + testTribalAuditPrivate(); + } + + // Login as Auditor + testLogoutGov(); + testLoginGovLogin(); + cy.visit(`/audit/submission-progress/${reportId}`); + }) + } + + // Complete the audit information form + cy.get(".usa-link").contains("Audit Information form").click(); + testAuditInformationForm(); + + cy.get(".usa-link").contains("Pre-submission validation").click(); + testCrossValidation(); + + // Auditor certification + cy.get(".usa-link").contains("Auditor Certification").click(); + testAuditorCertification(); + + // Grab the report ID from the URL + cy.url().then(url => { + const reportId = url.split('/').pop(); + + testLogoutGov(); + + // Login as Auditee + testLoginGovLogin( + LOGIN_TEST_EMAIL_AUDITEE, + LOGIN_TEST_PASSWORD_AUDITEE, + LOGIN_TEST_OTP_SECRET_AUDITEE + ); + + cy.visit(`/audit/submission-progress/${reportId}`); + + // Auditee certification + cy.get(".usa-link").contains("Auditee Certification").click(); + testAuditeeCertification(); + + // Submit + cy.get(".usa-link").contains("Submit to the FAC for processing").click(); + cy.url().should('match', /\/audit\/submission\/[0-9]{4}-[0-9]{2}-GSAFAC-[0-9]{10}/); + cy.get('#continue').click(); + cy.url().should('match', /\/audit\//); + + // The report ID should be found in the Completed Audits table + cy.get('.usa-table').contains( + 'caption', + 'The audits listed below have been submitted to the FAC for processing and may not be edited.', + ).siblings().contains('td', reportId); + + // The Report should not be in the dissemination table + if (isPublic) { + testReportIdFound(reportId); + } else { + testReportIdNotFound(reportId); + } + }); + + testLogoutGov(); +} diff --git a/backend/schemas/output/excel/json/federal-awards-workbook.json b/backend/schemas/output/excel/json/federal-awards-workbook.json index d7bc18de2c..6abc274083 100644 --- a/backend/schemas/output/excel/json/federal-awards-workbook.json +++ b/backend/schemas/output/excel/json/federal-awards-workbook.json @@ -458,7 +458,7 @@ }, { "format": "text", - "formula": "=IF(OR(B{0}=\"\",C{0}),\"\",CONCATENATE(B{0},\".\",C{0}))", + "formula": "=IF(OR(B{0}=\"\",C{0}=\"\"),\"\",CONCATENATE(B{0},\".\",C{0}))", "help": { "link": "https://fac.gov/documentation/validation/#unknown", "text": "Please contact support" diff --git a/backend/schemas/output/excel/xlsx/additional-eins-workbook.xlsx b/backend/schemas/output/excel/xlsx/additional-eins-workbook.xlsx index 663f07259b..203fcf0571 100644 Binary files a/backend/schemas/output/excel/xlsx/additional-eins-workbook.xlsx and b/backend/schemas/output/excel/xlsx/additional-eins-workbook.xlsx differ diff --git a/backend/schemas/output/excel/xlsx/additional-ueis-workbook.xlsx b/backend/schemas/output/excel/xlsx/additional-ueis-workbook.xlsx index 00e3730574..4ec5db4d18 100644 Binary files a/backend/schemas/output/excel/xlsx/additional-ueis-workbook.xlsx and b/backend/schemas/output/excel/xlsx/additional-ueis-workbook.xlsx differ diff --git a/backend/schemas/output/excel/xlsx/audit-findings-text-workbook.xlsx b/backend/schemas/output/excel/xlsx/audit-findings-text-workbook.xlsx index 50e4ff69ca..e48a9b2746 100644 Binary files a/backend/schemas/output/excel/xlsx/audit-findings-text-workbook.xlsx and b/backend/schemas/output/excel/xlsx/audit-findings-text-workbook.xlsx differ diff --git a/backend/schemas/output/excel/xlsx/corrective-action-plan-workbook.xlsx b/backend/schemas/output/excel/xlsx/corrective-action-plan-workbook.xlsx index 36a00ddfca..fee38399ba 100644 Binary files a/backend/schemas/output/excel/xlsx/corrective-action-plan-workbook.xlsx and b/backend/schemas/output/excel/xlsx/corrective-action-plan-workbook.xlsx differ diff --git a/backend/schemas/output/excel/xlsx/federal-awards-audit-findings-workbook.xlsx b/backend/schemas/output/excel/xlsx/federal-awards-audit-findings-workbook.xlsx index 3a7396f894..2c17b3a87e 100644 Binary files a/backend/schemas/output/excel/xlsx/federal-awards-audit-findings-workbook.xlsx and b/backend/schemas/output/excel/xlsx/federal-awards-audit-findings-workbook.xlsx differ diff --git a/backend/schemas/output/excel/xlsx/federal-awards-workbook.xlsx b/backend/schemas/output/excel/xlsx/federal-awards-workbook.xlsx index 70a765f607..a3be21383e 100644 Binary files a/backend/schemas/output/excel/xlsx/federal-awards-workbook.xlsx and b/backend/schemas/output/excel/xlsx/federal-awards-workbook.xlsx differ diff --git a/backend/schemas/output/excel/xlsx/notes-to-sefa-workbook.xlsx b/backend/schemas/output/excel/xlsx/notes-to-sefa-workbook.xlsx index 9b047f9d39..0e34ba47ba 100644 Binary files a/backend/schemas/output/excel/xlsx/notes-to-sefa-workbook.xlsx and b/backend/schemas/output/excel/xlsx/notes-to-sefa-workbook.xlsx differ diff --git a/backend/schemas/output/excel/xlsx/secondary-auditors-workbook.xlsx b/backend/schemas/output/excel/xlsx/secondary-auditors-workbook.xlsx index ae7dc8efb6..4eb47e915d 100644 Binary files a/backend/schemas/output/excel/xlsx/secondary-auditors-workbook.xlsx and b/backend/schemas/output/excel/xlsx/secondary-auditors-workbook.xlsx differ diff --git a/backend/schemas/output/sections/FederalAwards.schema.json b/backend/schemas/output/sections/FederalAwards.schema.json index c9e31b6855..e49cf0ec26 100644 --- a/backend/schemas/output/sections/FederalAwards.schema.json +++ b/backend/schemas/output/sections/FederalAwards.schema.json @@ -373,7 +373,7 @@ "if": { "properties": { "three_digit_extension": { - "pattern": "^(RD|U[0-9]{2})$", + "pattern": "^(RD[0-9]?|U[0-9]{2})$", "type": "string" } } @@ -438,7 +438,7 @@ "type": "string" }, "three_digit_extension": { - "pattern": "^(RD|[0-9]{3}[A-Za-z]{0,1}|U[0-9]{2})$", + "pattern": "^(RD[0-9]?|[0-9]{3}[A-Za-z]{0,1}|U[0-9]{2})$", "type": "string" } }, diff --git a/backend/schemas/source/base/Base.libsonnet b/backend/schemas/source/base/Base.libsonnet index 979052564c..6d7fdd6953 100644 --- a/backend/schemas/source/base/Base.libsonnet +++ b/backend/schemas/source/base/Base.libsonnet @@ -66,7 +66,8 @@ local Meta = { }; local REGEX_ALN_PREFIX = '^([0-9]{2})$'; -local REGEX_RD_EXTENSION = 'RD'; +# A python version of these regexes also exists in intakelib +local REGEX_RD_EXTENSION = 'RD[0-9]?'; local REGEX_THREE_DIGIT_EXTENSION = '[0-9]{3}[A-Za-z]{0,1}'; local REGEX_U_EXTENSION = 'U[0-9]{2}'; local REGEX_NUMBER = '[0-9]+'; diff --git a/backend/schemas/source/excel/templates/federal-awards-workbook.jsonnet b/backend/schemas/source/excel/templates/federal-awards-workbook.jsonnet index 36e43a514b..36d4b5260c 100644 --- a/backend/schemas/source/excel/templates/federal-awards-workbook.jsonnet +++ b/backend/schemas/source/excel/templates/federal-awards-workbook.jsonnet @@ -283,7 +283,7 @@ local open_ranges_defns = [ [ Sheets.open_range { keep_locked: true, - formula: '=IF(OR(B{0}="",C{0}),"",CONCATENATE(B{0},".",C{0}))', + formula: '=IF(OR(B{0}="",C{0}=""),"",CONCATENATE(B{0},".",C{0}))', width: 12, format: 'text', help: Help.unknown, diff --git a/docs/testing.md b/docs/testing.md index 5642fd0cd6..e7d30afe71 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -119,7 +119,17 @@ Like with Bandit, new code will need to pass all of these to be merged into the # End-to-end testing We use Cypress to do end-to-end testing of the application. Tests are defined -in files in [backend/cypress/e2e/](/backend/cypress/e2e). Run cypress with `npx cypress open`. +in files in [backend/cypress/e2e/](/backend/cypress/e2e). To run these tests: +- Run the app. Then, from the `FAC/backend` directory: +- `npm i` +- [Create a testing login.gov account](https://github.com/GSA-TTS/FAC/blob/main/docs/testing.md#testing-behind-logingov) +- [Set up the fac() alias](https://github.com/GSA-TTS/FAC/blob/main/docs/development.md?plain=1#L125) +- [Generate a new JWT](https://github.com/GSA-TTS/FAC/blob/main/backend/dissemination/README.md#creating-a-jwt-secret) +- `CYPRESS_LOGIN_TEST_EMAIL='