Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provinces #76

Closed
michaelbromley opened this issue Mar 11, 2019 · 10 comments
Closed

Provinces #76

michaelbromley opened this issue Mar 11, 2019 · 10 comments
Labels
design 📐 This issue deals with high-level design of a feature @vendure/core
Milestone

Comments

@michaelbromley
Copy link
Member

A Province is a subdivision of a Country. In the UK, it would be a county such as Greater Manchester, whereas in the USA it would be a state such as Connecticut.

Province data is useful for certain tax and shipping calculations. Currently we only support tax & shipping calculations based on country, but for certain use-cases this is not granular enough.

Use cases

State-based taxes

In the USA, sales taxes differ state-to-state, in a rather complex way as outlined by this article: https://www.taxjar.com/guides/intro-to-sales-tax/#steps-to-sales-tax-compliance.

Region-based shipping

In the UK, small parcels can be sent by Royal Mail for a fixed price. However, certain counties are designated "highlands & islands", including the Scottish Highlands, Jersey, Guernsey etc. Shipping to these locations is more expensive owing to the difficulty of access or remoteness. Therefore province-based shipping rules would apply within a single country.

@michaelbromley michaelbromley added @vendure/core design 📐 This issue deals with high-level design of a feature labels Mar 11, 2019
@agustif
Copy link
Contributor

agustif commented May 30, 2020

In spain usually shipping to the Islands (both Canary and Balearic), has a surplus on shipping

We also have province, which are also part of Comunidades Autonomas, I'm not sure how to handle this though, maybe we could have flexible defaults, and country-specific plugins from community

@michaelbromley
Copy link
Member Author

@agustif For now it would also be possible to set up a ShippingCalculator which inspects the postalCode of the shipping address (if this can be relied upon) to add a surplus. This method works in the UK case because the highlands & islands have a well-defined postal code range.

@cbnsndwch
Copy link
Contributor

This will be absolutely vital for US-based stores, as sales tax is significantly complex. In general, there up to three levels of sales tax in the US:

  • State-level taxes, which for 2020 are non-zero for all states except Alaska, Delaware, and New Hampshire (https://taxfoundation.org/2020-sales-taxes)
  • County-level taxes, which vary widely. This introduces the first complexity, as postal addresses can change the County they are in from time to time (albeit infrequently)
  • Local taxes, imposed by cities and other 3rd level administrative authorities. This is the next source of complexity. Some postal codes (ZIP Codes) fall within more than one County/City, and the right local tax rate must be resolved for the address on a case-by-case basis.

Because there is no sales tax collection at the federal (country) level, businesses must only collect and remit taxes in the states where they have an "economic nexus" which, in a non-exhaustive sense, typically means one of:

  • The business has a physical presence in the state (an office, a retail location, a warehouse, a distribution center operated by the business itself or a partner, etc.)
  • The business had sales into the state exceeding a certain threshold in the past tax period (e.g. a fiscal or natural year).

I see two viable solutions here:

  • An async function that gets passed the full order (items + adjustments) and the shipping address, and calculates the appropriate tax via its own logic or by calling an external service (TaxJar, Avalara AvaTax, etc.)
  • Let the admin specify static tax rates per product/sku/category/channel, and limit their visibility. This might not work in all cases.

@ashitikov
Copy link
Contributor

I think it worth to implement some generic mechanism, that will support deep level of locations, for example Zone -> Country -> Province/Area -> City -> Street -> House. This can be useful for taxes and for shipping calculations, especially where e-commerce is local, and shipping price depends on more precise location.

@michaelbromley
Copy link
Member Author

@ashitikov for such fine-grained calculations as based on house or even street, do you not think it would be better to just use a custom function rather than create entities for these? I can imagine it being somewhat workable down to the city level, but deeper than that and you'll need a massive postal address database embedded in your data model.

@ashitikov
Copy link
Contributor

@michaelbromley There is no problem to use custom functions and entities, but having built-in native mechanism is great. Vendure allows developers to extend it deeply, introducing new entities, fields and do some injections in config - that is great, and I believe vendure can do more ;). There are a lot of cases when developer needs a flexible system to calculate tax and delivery price more precisely. Province is just the one of such cases.

I have an idea, its not final, but I'll share it:
What if we add new entity called 'Place', which is going to be a base class, then inherit entities 'Zone', 'Country' and 'Province' from 'Place' using single table-inheritance https://typeorm.io/entity-inheritance#single-table-inheritance . In fact this would be a single table with a special field 'type'. This field will be used by TypeORM to determine concrete type of entity and instantiate a registered class during fetch.

From the developer perspective: you have 3 different classes and one base class. If you want to extend built-in places, register 4-th new class with ChildEntity() decorator and add it to plugin's entities. Also you may want to introduce custom graphql resolvers and types for an entity.

From vendure's core perspective: tax and delivery calculation systems have to use 'Place' entity and pass it when required to calculators/handlers and etc... Instead of Countries, Zones, Provinces, we are working with Places.

From vendure's admin-ui perspective: zones and countries may be dropped to use places instead. A list of places may be filtered by types. There are some complications with country-zone relations, but I believe anyway this is workable solution. Extended entities from places would also be visible and managed in the list.

This way, e-commerce stores may develop plugins for advanced tax calculation, as well as for advanced delivery.

Note:
I'm not sure about single table inheritance design. This could be replaced with base interface and standard entities without inheritance.

@michaelbromley michaelbromley added this to the v2.0 milestone Jul 1, 2022
@michaelbromley michaelbromley moved this to 📅 Planned in Vendure OS Roadmap Jul 1, 2022
@michaelbromley michaelbromley moved this from 📅 Planned to 🏗 In progress in Vendure OS Roadmap Apr 12, 2023
michaelbromley added a commit that referenced this issue Apr 13, 2023
Relates to #76

BREAKING CHANGE: A new `Region` entity has been introduced, which is a base class for `Country` and
the new `Province` entity. The `Zone.members` property is now an array of `Region` rather than
`Country`, since Zones may now be composed of both countries and provinces. If you have defined
any custom fields on `Country`, you'll need to change it to `Region` in your custom fields config.
@michaelbromley
Copy link
Member Author

The data model for Provinces support is now in place - it is similar to the suggestion by @ashitikov, but keeps the Zone entity as a separate model, and introduces the abstract Region entity which is extended by the Country and Province entities, with the possibility for new, custom region types to be added in userland.

What's still missing is an Admin UI part to manage provinces - this will be included in the general ongoing Admin UI refresh work.

@bojanz
Copy link

bojanz commented May 4, 2023

It's probably too late for me to affect any architecture, but I might at least offer some insight, as I've now created popular solutions in this area in both Go and PHP, both taking after Google's libaddressinput which has been the golden standard in this area.

Lessons I've learned over the years:

  1. You will want to predefine regions for as many countries as possible. Having to use a UI for manual entry gets old very quickly. Google provides a big public domain list of regions, and it can be extended by data taken directly from ISO. Users can then extend these lists using custom code or a UI.

  2. Regions usually aren't translated, but they do often have a variant in local script.

  3. It is generally wise to couple regions to the concept of an "address format" and include other per-country information there (used fields, postal code regexp, etc).

  4. Developers expect to be able to have an endpoint that retrieves all supported address formats and their regions (or just the regions if the system has no concept of an address format). This endpoint can then be used to power frontend widgets, and/or as a reference to indicate which region codes/IDs an API accepts.

  5. Google's libaddressinput and my PHP library also support regions on multiple levels, allowing countries such as China/Brazil/South Korea/etc to predefine localities and sublocalities, but I feel like there are diminishing returns there, as the data source becomes large and hard to maintain. That's why my Go library focuses only on a single level (administrative areas). I also wrote a blog post when announcing the Go version that goes into some more detail about the differences from libaddressinput.

When it comes to zones, being able to define a zone spanning different regions is a good first step, but there are many use cases where dropping down to the postal code level (included/excluded by range or regexp) is needed.

Happy to chat about this and other topics if you feel I can be of service.

@matteofrana
Copy link

In Italy shipping costs depend also on zip codes: usually you have a fixed price for weight range (for example 1-3 Kg), with the exception of smaller Islands, Venice lagoon, Livigno (tax-free) and Campione d'Italia (which actually is in Swiss). These exceptions have a much higher cost.
The only way to identify these places is by Zip Code, there is no way to identify them exactly by city or province.

@michaelbromley
Copy link
Member Author

@matteofrana Hi! Yes we have something similar in the UK with the Scottish Highlands and also the islands of Jersey & Guernsey. This case can be handled by specific logic in a ShippingCalculator.

@michaelbromley michaelbromley moved this from 🏗 In progress to ✅ Done in Vendure OS Roadmap Jun 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design 📐 This issue deals with high-level design of a feature @vendure/core
Projects
Archived in project
Development

No branches or pull requests

6 participants