Skip to content

Conversation

@pierredup
Copy link
Member

@pierredup pierredup commented May 3, 2025

Remove intermediate entity for mapping quotes and invoices to users. This simplifies the users on quotes and invoices by a lot, and prevents subtle bugs and errors (E.G users were always duplicated on an invoice and quote when editing the quote/invoice). One drawback is that we don't save the company_id on the (invoice|quote)_contact table anymore, but generally this shouldn't be an issue since we're only loading the users for an active client based on the current company (so we should never land in a situation where invalid/unrelated users are added to a quote/invoice)

Summary by CodeRabbit

  • New Features

    • Improved forms for invoices, recurring invoices, and quotes now dynamically update the "users" field based on the selected client, providing a more intuitive and responsive user experience.
  • Refactor

    • Simplified user associations for invoices, recurring invoices, and quotes by directly linking contacts, making user management more straightforward.
    • Streamlined the process of adding or removing users from invoices, recurring invoices, and quotes.
  • Chores

    • Removed obsolete internal components and tests related to previous user association methods.

@pierredup pierredup added the bug label May 3, 2025
@pierredup pierredup added this to the 2.3.5 milestone May 3, 2025
@pierredup pierredup self-assigned this May 3, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 3, 2025

Walkthrough

This update refactors the user-contact relationship model for invoices, recurring invoices, and quotes. It removes intermediary entities (InvoiceContact, RecurringInvoiceContact, QuoteContact), replacing one-to-many associations with direct many-to-many relationships between Contact and the primary entities. All related code, including form event subscribers, model transformers, repositories, and tests for the intermediary entities, is deleted. The form logic is updated to use dynamic dependent fields for user selection based on the client, removing the need for event subscribers and registry dependencies. Test cases are adjusted to match the new constructor signatures and entity relationships. Cloning and management logic now add users individually.

Changes

File(s) / Group Change Summary
src/ClientBundle/Entity/Contact.php, src/InvoiceBundle/Entity/Invoice.php, Refactored user associations from one-to-many with intermediary entities to direct many-to-many with Contact. Updated Doctrine mappings, property types, and getter methods. Added addUser/removeUser methods; removed setUsers.
src/InvoiceBundle/Entity/RecurringInvoice.php, src/QuoteBundle/Entity/Quote.php Same as above: replaced intermediary entities with direct many-to-many to Contact, updated mapping, added addUser/removeUser, removed setUsers, simplified getters. Also changed property visibility in RecurringInvoice.
src/InvoiceBundle/Entity/InvoiceContact.php, src/InvoiceBundle/Entity/RecurringInvoiceContact.php, src/QuoteBundle/Entity/QuoteContact.php Deleted intermediary entity classes and their API/resource annotations, properties, methods, and ORM mappings.
src/InvoiceBundle/Repository/RecurringInvoiceContactRepository.php Deleted repository class for the intermediary entity.
src/CoreBundle/Form/Transformer/UserToContactTransformer.php, src/InvoiceBundle/Form/EventListener/InvoiceUsersSubscriber.php, src/QuoteBundle/Form/EventListener/QuoteUsersSubscriber.php Deleted model transformer and event subscriber classes for user/contact form management.
src/InvoiceBundle/Form/Type/InvoiceType.php, src/InvoiceBundle/Form/Type/RecurringInvoiceType.php, src/QuoteBundle/Form/Type/QuoteType.php Refactored form types to use DynamicFormBuilder and dependent fields for user selection based on client, removing event subscribers and registry dependencies. Updated constructor signatures.
src/InvoiceBundle/Manager/InvoiceManager.php, src/InvoiceBundle/Cloner/InvoiceCloner.php, src/QuoteBundle/Cloner/QuoteCloner.php Updated logic to add users individually using addUser() instead of setting users in bulk.
src/CoreBundle/Tests/Form/Transformer/UserToContactTransformerTest.php Deleted test class for the removed model transformer.
src/InvoiceBundle/Tests/Form/Handler/InvoiceCreateHandlerTest.php, src/QuoteBundle/Tests/Form/Handler/QuoteCreateHandlerTest.php, src/FormBundle/Test/FormHandlerTestCase.php, src/InvoiceBundle/Tests/Form/Type/InvoiceTypeTest.php, src/QuoteBundle/Tests/Form/Type/QuoteTypeTest.php Updated test setup: removed registry arguments from form type constructors to match new signatures.
src/InvoiceBundle/Resources/config/services/services.php, src/QuoteBundle/Resources/config/services/services.php Removed service imports and removals for deleted event subscribers.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Form
    participant DynamicFormBuilder
    participant DB

    User->>Form: Open Invoice/Quote form
    Form->>DynamicFormBuilder: Build form
    DynamicFormBuilder->>DB: Fetch clients
    User->>Form: Select client
    DynamicFormBuilder->>DB: Fetch contacts for selected client
    Form->>User: Show users field with contacts
    User->>Form: Select users (contacts)
    Form->>DB: Save Invoice/Quote with selected contacts (many-to-many)
Loading

Poem

In fields of forms where contacts grew,
The middle links have bid adieu!
Now many-to-many, direct and bright,
Users and contacts join with delight.
No more wrappers, no more fuss,
The code is lean—just like us!
🐇✨

✨ Finishing Touches
  • 📝 Docstrings were successfully generated. (🔄 Check again to generate docstrings again)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@pierredup
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 3, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov
Copy link

codecov bot commented May 3, 2025

Bundle Report

Changes will decrease total bundle size by 407.05kB (-5.1%) ⬇️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
solidinvoice-webpack-bundle-array-push 7.57MB -407.05kB (-5.1%) ⬇️

Affected Assets, Files, and Routes:

view changes for bundle: solidinvoice-webpack-bundle-array-push

Assets Changed:

Asset Name Size Change Total Size Change (%)
app.*.css -555 bytes 1.36MB -0.04%
email.*.css -152.22kB 504.16kB -23.19%
pdf.*.css -152.22kB 445.21kB -25.48%
648.*.js (New) 376.53kB 376.53kB 100.0% 🚀
145.*.css (New) 297.58kB 297.58kB 100.0% 🚀
runtime.*.js -1 bytes 3.21kB -0.03%
core.*.js 53 bytes 3.1kB 1.74%
core.*.css 311 bytes 1.33kB 30.43% ⚠️
355.*.js (Deleted) -446.4kB 0 bytes -100.0% 🗑️
847.*.css (Deleted) -330.13kB 0 bytes -100.0% 🗑️

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (5)
src/InvoiceBundle/Form/Type/RecurringInvoiceType.php (1)

51-61: Consider using a query-builder instead of eager loading all clients

Fetching all clients via findAll() loads every record into memory, which can become costly once the client table grows.
Prefer passing a query_builder closure so Doctrine can stream results or paginate if needed. This is consistent with the pattern already used for the users field below.

-                'choices' => $this->registry->getRepository(Client::class)->findAll()
+                'query_builder' => fn (EntityRepository $r) => $r->createQueryBuilder('c')->orderBy('c.name', 'ASC')
src/InvoiceBundle/Form/Type/InvoiceType.php (1)

54-55: Minor: re-using the $builder var hides the original builder

new DynamicFormBuilder($builder) returns a decorator; shadowing the parameter is harmless but slightly confusing when stepping through with a debugger.
Consider assigning to a separate variable ($dynBuilder) or injecting BuilderDecorator via composition to improve readability.

src/QuoteBundle/Form/Type/QuoteType.php (1)

55-56: Variable shadowing of $builder

For consistency with the other form types you might rename the decorated instance to avoid confusion (see remark in InvoiceType).

src/ClientBundle/Entity/Contact.php (2)

218-233: Consider aligning association naming and provide helper mutators

The inverse side is mapped by the users field on the owning entities (Invoice, RecurringInvoice, Quote). Because this entity is called Contact, consumers may find the users naming unintuitive (it still reflects the old “user-contact” abstraction).
If renaming on the owning side is not feasible right now, at minimum add a short PHPDoc comment explaining the historical reason so that new contributors do not look for a missing contacts property on those entities.

In addition, exposing the raw Collection without add*/remove* helpers makes it harder to keep the bidirectional relation in sync from this side and invites accidental writes to the inverse side. Even if this side stays inverse-only, explicit helpers that delegate to the owning side improve DX and make intent obvious.


379-396: Return typed, read-only collections or add defensive cloning

getInvoices(), getRecurringInvoices(), and getQuotes() return the live ArrayCollection instance.
External callers can therefore mutate the collection directly (e.g. $contact->getInvoices()->clear()), breaking the invariant that only the owning side should manipulate the relationship.

Two lightweight options:

-    public function getInvoices(): Collection
+    /**
+     * Returns an immutable view of the invoices collection to prevent
+     * accidental modification from the inverse side.
+     */
+    public function getInvoices(): Collection

or, if PHPStan/Psalm is used, change the return type to Collection<int, Invoice>&\Doctrine\Common\Collections\ReadonlyCollection.

Alternatively, expose dedicated addInvoice() / removeInvoice() that proxy to $invoice->addUser($this) and return a read-only view from the getter.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 902e248 and fd4de2d.

📒 Files selected for processing (25)
  • src/ClientBundle/Entity/Contact.php (2 hunks)
  • src/CoreBundle/Form/Transformer/UserToContactTransformer.php (0 hunks)
  • src/CoreBundle/Tests/Form/Transformer/UserToContactTransformerTest.php (0 hunks)
  • src/FormBundle/Test/FormHandlerTestCase.php (1 hunks)
  • src/InvoiceBundle/Cloner/InvoiceCloner.php (1 hunks)
  • src/InvoiceBundle/Entity/Invoice.php (2 hunks)
  • src/InvoiceBundle/Entity/InvoiceContact.php (0 hunks)
  • src/InvoiceBundle/Entity/RecurringInvoice.php (3 hunks)
  • src/InvoiceBundle/Entity/RecurringInvoiceContact.php (0 hunks)
  • src/InvoiceBundle/Form/EventListener/InvoiceUsersSubscriber.php (0 hunks)
  • src/InvoiceBundle/Form/Type/InvoiceType.php (3 hunks)
  • src/InvoiceBundle/Form/Type/RecurringInvoiceType.php (2 hunks)
  • src/InvoiceBundle/Manager/InvoiceManager.php (1 hunks)
  • src/InvoiceBundle/Repository/RecurringInvoiceContactRepository.php (0 hunks)
  • src/InvoiceBundle/Resources/config/services/services.php (0 hunks)
  • src/InvoiceBundle/Tests/Form/Handler/InvoiceCreateHandlerTest.php (1 hunks)
  • src/InvoiceBundle/Tests/Form/Type/InvoiceTypeTest.php (1 hunks)
  • src/QuoteBundle/Cloner/QuoteCloner.php (1 hunks)
  • src/QuoteBundle/Entity/Quote.php (2 hunks)
  • src/QuoteBundle/Entity/QuoteContact.php (0 hunks)
  • src/QuoteBundle/Form/EventListener/QuoteUsersSubscriber.php (0 hunks)
  • src/QuoteBundle/Form/Type/QuoteType.php (3 hunks)
  • src/QuoteBundle/Resources/config/services/services.php (0 hunks)
  • src/QuoteBundle/Tests/Form/Handler/QuoteCreateHandlerTest.php (1 hunks)
  • src/QuoteBundle/Tests/Form/Type/QuoteTypeTest.php (1 hunks)
💤 Files with no reviewable changes (10)
  • src/InvoiceBundle/Resources/config/services/services.php
  • src/QuoteBundle/Resources/config/services/services.php
  • src/InvoiceBundle/Repository/RecurringInvoiceContactRepository.php
  • src/QuoteBundle/Entity/QuoteContact.php
  • src/InvoiceBundle/Entity/InvoiceContact.php
  • src/CoreBundle/Form/Transformer/UserToContactTransformer.php
  • src/CoreBundle/Tests/Form/Transformer/UserToContactTransformerTest.php
  • src/InvoiceBundle/Entity/RecurringInvoiceContact.php
  • src/InvoiceBundle/Form/EventListener/InvoiceUsersSubscriber.php
  • src/QuoteBundle/Form/EventListener/QuoteUsersSubscriber.php
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/QuoteBundle/Cloner/QuoteCloner.php (1)
src/QuoteBundle/Entity/Quote.php (2)
  • getUsers (274-277)
  • addUser (279-286)
src/FormBundle/Test/FormHandlerTestCase.php (3)
src/InvoiceBundle/Form/Type/InvoiceType.php (1)
  • InvoiceType (41-137)
src/CoreBundle/Generator/BillingIdGenerator.php (1)
  • BillingIdGenerator (23-58)
src/QuoteBundle/Form/Type/QuoteType.php (1)
  • QuoteType (40-136)
⏰ Context from checks skipped due to timeout of 90000ms (14)
  • GitHub Check: DB (PostgreSQL pdo_pgsql 16)
  • GitHub Check: DB (MySQL pdo_mysql 5.7)
  • GitHub Check: DB (PostgreSQL pdo_pgsql 17)
  • GitHub Check: DB (MariaDB pdo_mysql 10.11)
  • GitHub Check: DB (MariaDB pdo_mysql 11.4)
  • GitHub Check: DB (MariaDB pdo_mysql 11)
  • GitHub Check: DB (MariaDB pdo_mysql 11.0)
  • GitHub Check: DB (MariaDB pdo_mysql 10.6)
  • GitHub Check: DB (MariaDB pdo_mysql 10.5)
  • GitHub Check: DB (MySQL pdo_mysql 8.4)
  • GitHub Check: DB (MySQL pdo_mysql 8.0)
  • GitHub Check: DB (MySQL pdo_mysql 9)
  • GitHub Check: DB (MySQL pdo_mysql 8.3)
  • GitHub Check: Unit ( PHP 8.2 )
🔇 Additional comments (11)
src/InvoiceBundle/Manager/InvoiceManager.php (1)

122-124: Updated user assignment to follow individual entity management pattern

The change from bulk users assignment to individual user addition follows best practices for managing collections in Doctrine entities. This maintains proper relationship tracking and avoids potential issues with the collection's internal state.

src/QuoteBundle/Tests/Form/Handler/QuoteCreateHandlerTest.php (1)

225-225: Constructor signature updated to reflect form type changes

The QuoteType constructor signature was properly updated to remove the ManagerRegistry dependency, aligning with the refactoring that replaces event subscribers with dynamic dependent fields for user selection.

src/InvoiceBundle/Tests/Form/Handler/InvoiceCreateHandlerTest.php (1)

222-222: Constructor signature updated to reflect form type changes

The InvoiceType constructor signature was properly updated to remove the ManagerRegistry dependency, aligning with the broader refactoring that simplifies form handling and entity relationships.

src/QuoteBundle/Cloner/QuoteCloner.php (1)

59-61: Updated user assignment to follow collection management best practices

The change from bulk users assignment to individual user addition properly handles the new direct many-to-many relationship between Quote and Contact. This approach ensures proper entity tracking by Doctrine and matches the implementation in the InvoiceManager.

src/InvoiceBundle/Cloner/InvoiceCloner.php (1)

67-69: User association logic updated to use addUser method

The code now iterates through each user in the original invoice and adds them individually to the new invoice using the addUser() method. This change aligns with the broader refactoring that removed the intermediate entity and switched from bulk assignment to direct management of many-to-many relationships.

src/FormBundle/Test/FormHandlerTestCase.php (1)

80-81: Constructor calls updated to match new signatures

The constructor calls for InvoiceType and QuoteType have been updated to remove the $this->registry parameter. This change correctly aligns with the updated form type implementations where ManagerRegistry dependency was removed in favor of dynamic dependent fields for user selection.

src/QuoteBundle/Tests/Form/Type/QuoteTypeTest.php (1)

69-74: QuoteType instantiation updated to match new constructor signature

The QuoteType instantiation has been modified to provide only the required parameters ($systemConfig and BillingIdGenerator), removing the $this->registry parameter. This correctly aligns with the refactored QuoteType class that no longer uses the registry for event subscribers.

src/InvoiceBundle/Tests/Form/Type/InvoiceTypeTest.php (1)

101-106: InvoiceType instantiation updated to match new constructor signature

The InvoiceType instantiation has been updated to provide only the required parameters ($systemConfig and BillingIdGenerator), removing the $this->registry parameter. This change properly reflects the refactored InvoiceType class that no longer relies on the registry for managing user fields.

src/QuoteBundle/Entity/Quote.php (1)

225-233: Validate mapping & cascades for the new Many-to-Many association

The new association is now the owning side (inversedBy="quotes") with an explicit join table.
Two points to double-check:

  1. If a brand-new Contact is ever created via the Quote API payload, it will not be automatically persisted because no cascade is configured (cascade:["persist"]).
    This is probably fine (contacts are usually created elsewhere) – just be sure this behaviour is intentional.

  2. Make sure the inverse side in Contact::$quotes is declared with mappedBy="users". A mismatch will raise a doctrine‐mapping error at runtime.

src/InvoiceBundle/Entity/Invoice.php (1)

175-183: Migration note – join-table re-use

invoice_contact already exists (previously backed by the InvoiceContact entity).
Because the old table contained extra columns (id, company_id, …), simply re-using the name will break hydration unless a migration drops those columns first.

Please verify that the accompanying migration removes/renames the obsolete columns or picks a fresh table name.

src/InvoiceBundle/Entity/RecurringInvoice.php (1)

112-125: Confirm inverse mapping & legacy table clean-up

The new join table recurringinvoice_contacts replaces the old recurringinvoice_contact entity table.
Ensure:

  1. Contact::$recurringInvoices is mapped with mappedBy="users".
  2. The migration drops the legacy table or its extra columns to avoid mapping conflicts.

@pierredup pierredup force-pushed the simplify-billing-users branch from fd4de2d to a52243c Compare May 3, 2025 19:11
@codecov
Copy link

codecov bot commented May 3, 2025

Codecov Report

Attention: Patch coverage is 70.83333% with 35 lines in your changes missing coverage. Please review.

Project coverage is 48.77%. Comparing base (4e5ca6d) to head (fb8db64).
Report is 5 commits behind head on 2.3.x.

Files with missing lines Patch % Lines
src/QuoteBundle/Form/Type/QuoteType.php 27.77% 13 Missing ⚠️
src/ClientBundle/Entity/Contact.php 66.66% 9 Missing ⚠️
src/InvoiceBundle/Entity/RecurringInvoice.php 60.00% 4 Missing ⚠️
src/InvoiceBundle/Entity/Invoice.php 66.66% 3 Missing ⚠️
src/QuoteBundle/Entity/Quote.php 75.00% 3 Missing ⚠️
src/InvoiceBundle/Cloner/InvoiceCloner.php 50.00% 1 Missing ⚠️
src/InvoiceBundle/Manager/InvoiceManager.php 50.00% 1 Missing ⚠️
src/QuoteBundle/Cloner/QuoteCloner.php 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##              2.3.x    #1668      +/-   ##
============================================
- Coverage     49.13%   48.77%   -0.36%     
+ Complexity     2706     2685      -21     
============================================
  Files           498      491       -7     
  Lines          9721     9654      -67     
============================================
- Hits           4776     4709      -67     
  Misses         4945     4945              
Flag Coverage Δ
unittests 48.77% <70.83%> (-0.36%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

coderabbitai bot added a commit that referenced this pull request May 3, 2025
Docstrings generation was requested by @pierredup.

* #1668 (comment)

The following files were modified:

* `migrations/Version20305.php`
* `src/ClientBundle/Entity/Contact.php`
* `src/FormBundle/Test/FormHandlerTestCase.php`
* `src/InvoiceBundle/Cloner/InvoiceCloner.php`
* `src/InvoiceBundle/Entity/Invoice.php`
* `src/InvoiceBundle/Entity/RecurringInvoice.php`
* `src/InvoiceBundle/Form/Type/InvoiceType.php`
* `src/InvoiceBundle/Form/Type/RecurringInvoiceType.php`
* `src/InvoiceBundle/Manager/InvoiceManager.php`
* `src/QuoteBundle/Cloner/QuoteCloner.php`
* `src/QuoteBundle/Entity/Quote.php`
* `src/QuoteBundle/Form/Type/QuoteType.php`
@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 3, 2025

Note

Generated docstrings for this pull request at #1669

@pierredup pierredup merged commit 8df05b1 into 2.3.x May 3, 2025
51 of 56 checks passed
@pierredup pierredup deleted the simplify-billing-users branch May 3, 2025 19:50
@coderabbitai coderabbitai bot mentioned this pull request May 4, 2025
This was referenced May 26, 2025
@coderabbitai coderabbitai bot mentioned this pull request Jun 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants