diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index a4433edf11..bec79b268e 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -2,7 +2,8 @@ name: "\U0001F41B Bug report" about: Report something that's broken. title: '' -labels: bug,unconfirmed +type: bug +projects: ['lunarphp/9'] assignees: '' --- diff --git a/.github/workflows/document-facades.yml b/.github/workflows/document-facades.yml index a5ce7528f1..c3780a2101 100644 --- a/.github/workflows/document-facades.yml +++ b/.github/workflows/document-facades.yml @@ -2,9 +2,9 @@ name: document-facades on: - push: - branches: - - '*.x' +# push: +# branches: +# - '*.x' workflow_dispatch: permissions: diff --git a/.github/workflows/fix-code-style.yml b/.github/workflows/fix-code-style.yml index 517d722242..695a8097e3 100644 --- a/.github/workflows/fix-code-style.yml +++ b/.github/workflows/fix-code-style.yml @@ -1,7 +1,7 @@ name: fix-code-style on: - push: + pull_request: jobs: fix-code-style: diff --git a/.github/workflows/split_packages.yml b/.github/workflows/split_packages.yml index 4bd58029d1..401acbe432 100644 --- a/.github/workflows/split_packages.yml +++ b/.github/workflows/split_packages.yml @@ -1,65 +1,34 @@ -name: Split Packages +name: monorepo-split + on: push: - branches: - - main - - "0.*" - - "1.*" - tags: - - "*" + tags: '*' + jobs: - tag_split_packages: + split-monorepo: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - package: ["admin", "core", "opayo", "paypal", "table-rate-shipping", "stripe", "meilisearch", "search"] + package: + - "admin" + - "core" + - "opayo" + - "paypal" + - "table-rate-shipping" + - "stripe" + - "search" steps: - uses: actions/checkout@v4 - - uses: shivammathur/setup-php@v2 - with: - php-version: 8.2 - coverage: none - - - name: Split ${{ matrix.package }} - uses: "danharrin/monorepo-split-github-action@v2.3.0" - if: "!startsWith(github.ref, 'refs/tags/')" - env: - GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} - with: - package_directory: "packages/${{ matrix.package }}" - repository_organization: "lunarphp" - repository_name: "${{ matrix.package }}" - user_name: "GitHub Action" - user_email: "action@github.com" - branch: ${GITHUB_REF#refs/heads/} - - - name: Set env - if: "startsWith(github.ref, 'refs/tags/')" - run: echo "GITHUB_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - - name: Split ${{ matrix.package }} - uses: "danharrin/monorepo-split-github-action@v2.3.0" - if: "startsWith(github.ref, 'refs/tags/')" - env: - GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} - with: - package_directory: "packages/${{ matrix.package }}" - repository_organization: "lunarphp" - repository_name: "${{ matrix.package }}" - user_name: "GitHub Action" - user_email: "action@github.com" - branch: ${GITHUB_TAG%.*} - - - name: Tag ${{ matrix.package }} - if: "startsWith(github.ref, 'refs/tags/')" - uses: "danharrin/monorepo-split-github-action@v2.3.0" + - name: Monorepo Split of ${{ matrix.package }} + uses: danharrin/monorepo-split-github-action@v2.4.0 env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} with: - tag: ${GITHUB_REF#refs/tags/} package_directory: "packages/${{ matrix.package }}" repository_organization: "lunarphp" repository_name: "${{ matrix.package }}" + branch: 1.x + tag: ${{ github.ref_name }} user_name: "GitHub Action" user_email: "action@github.com" - branch: ${GITHUB_TAG%.*} diff --git a/README.md b/README.md index b3658ef311..b80df30e68 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -
+ -[Lunar](https://lunarphp.io) is a set of Laravel packages that bring functionality akin to Shopify and other e-commerce platforms to +[Lunar](https://lunarphp.com) is a set of Laravel packages that bring functionality akin to Shopify and other e-commerce platforms to Laravel. You have complete freedom to create your own storefront(s), but we've already done the hard work for you in the backend. @@ -8,7 +8,7 @@ This repository serves as a monorepo for the main packages that make up Lunar. ## Documentation -- [v1.0 documentation](https://docs-v1.lunarphp.io/) +- [v1.0 documentation](https://docs.lunarphp.com/) ## Contribution @@ -18,8 +18,8 @@ This repository serves as a monorepo for the main packages that make up Lunar. ## Community -- [Join our discord server](https://discord.gg/v6qVWaf) and chat to the developers and people using Lunar. -- [We have a roadmap](https://github.com/orgs/lunarphp/projects/8) where we will be detailing which features are next. +- [Join our discord server](https://lunarphp.com/discord) and chat to the developers and people using Lunar. +- [We have a roadmap](https://github.com/orgs/lunarphp/projects/9) where we will be detailing which features are next. ## Packages in this monorepo diff --git a/composer.json b/composer.json index 0e116cd0ba..290346940e 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "awcodes/shout": "^2.0.4", "barryvdh/laravel-dompdf": "^3.0", "cartalyst/converter": "^9.0|^10", - "doctrine/dbal": "^3.6", + "doctrine/dbal": "^3.6|^4.0", "dompdf/dompdf": "^3.1", "ext-bcmath": "*", "ext-exif": "*", diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 5506568240..0000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -.vitepress/cache -.vitepress/dist diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js deleted file mode 100644 index 484bbef2d4..0000000000 --- a/docs/.vitepress/config.js +++ /dev/null @@ -1,195 +0,0 @@ -import {defineConfig} from 'vitepress' - -// https://vitepress.dev/reference/site-config -export default defineConfig({ - title: "Lunar", - description: "Laravel Headless E-Commerce", - head: [ - [ - 'link', - {rel: 'icon', href: '/icon.svg', type: 'image/svg'} - ], - [ - 'link', - {rel: 'preconnect', href: 'https://fonts.googleapis.com'} - ], - [ - 'link', - {rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: ''} - ], - [ - 'link', - { - rel: 'stylesheet', - href: 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;900&display=swap' - } - ], - [ - 'script', - { - src: 'https://cdn.usefathom.com/script.js', - 'data-spa': 'auto', - 'data-site': 'KMZGQTYE', - defer: '' - } - ], - [ - 'script', - { - async: '', - src: 'https://tally.so/widgets/embed.js' - } - ] - // would render: - ], - - appearance: 'dark', - - themeConfig: { - // https://vitepress.dev/reference/default-theme-config - logo: { - light: '/icon.svg', - dark: '/icon-dark.svg', - }, - - nav: [ - { - text: 'Core', - link: '/core/overview', - activeMatch: '/core/' - }, - { - text: 'Admin Panel', - link: '/admin/overview', - activeMatch: '/admin/' - }, - { - text: 'Support', - link: '/support', - activeMatch: '/support' - }, - { - text: 'Resources', - items: [ - {text: 'Add-ons', link: 'https://github.com/lunarphp/awesome'}, - {text: 'Discord', link: 'https://discord.gg/v6qVWaf'}, - {text: 'Discussions', link: 'https://github.com/lunarphp/lunar/discussions'} - ] - }, - { - text: '1.x', - items: [ - {text: 'Changelog', link: '/core/upgrading'}, - {text: 'Contributing', link: '/core/contributing'}, - {text: 'Roadmap', link: 'https://github.com/orgs/lunarphp/projects/8'}, - {text: '0.x Docs', link: 'https://v0.lunarphp.io/'}, - ] - } - ], - - sidebar: { - // This sidebar gets displayed when a user - // is on `guide` directory. - '/core/': [ - { - text: 'Getting Started', - collapsed: false, - items: [ - {text: 'Overview', link: '/core/overview'}, - {text: 'Installation', link: '/core/installation'}, - {text: 'Starter Kits', link: '/core/starter-kits'}, - {text: 'Configuration', link: '/core/configuration'}, - {text: 'Upgrade Guide', link: '/core/upgrading'}, - {text: 'Security', link: '/core/securing-your-site'}, - {text: 'Contributing', link: '/core/contributing'} - ] - }, - { - text: 'Reference', - collapsed: false, - items: [ - {text: 'Activity Log', link: '/core/reference/activity-log'}, - {text: 'Addresses', link: '/core/reference/addresses'}, - {text: 'Associations', link: '/core/reference/associations'}, - {text: 'Attributes', link: '/core/reference/attributes'}, - {text: 'Carts', link: '/core/reference/carts'}, - {text: 'Channels', link: '/core/reference/channels'}, - {text: 'Collections', link: '/core/reference/collections'}, - {text: 'Currencies', link: '/core/reference/currencies'}, - {text: 'Customers', link: '/core/reference/customers'}, - {text: 'Discounts', link: '/core/reference/discounts'}, - {text: 'Languages', link: '/core/reference/languages'}, - {text: 'Media', link: '/core/reference/media'}, - {text: 'Orders', link: '/core/reference/orders'}, - {text: 'Payments', link: '/core/reference/payments'}, - {text: 'Pricing', link: '/core/reference/pricing'}, - {text: 'Products', link: '/core/reference/products'}, - {text: 'Search', link: '/core/reference/search'}, - {text: 'Tags', link: '/core/reference/tags'}, - {text: 'Taxation', link: '/core/reference/taxation'}, - {text: 'URLs', link: '/core/reference/urls'} - ] - }, - { - text: 'Storefront', - collapsed: false, - items: [ - {text: 'Storefront Session', link: '/core/storefront-utils/storefront-session'}, - ] - }, - { - text: 'Extending', - collapsed: false, - items: [ - {text: 'Carts', link: '/core/extending/carts'}, - {text: 'Discounts', link: '/core/extending/discounts'}, - {text: 'Models', link: '/core/extending/models'}, - {text: 'Orders', link: '/core/extending/orders'}, - {text: 'Payments', link: '/core/extending/payments'}, - {text: 'Search', link: '/core/extending/search'}, - {text: 'Shipping', link: '/core/extending/shipping'}, - {text: 'Taxation', link: '/core/extending/taxation'} - ] - } - ], - - '/admin/': [ - { - text: 'Getting Started', - collapsed: false, - items: [ - {text: 'Overview', link: '/admin/overview'}, - ] - }, - { - text: 'Extending', - collapsed: false, - items: [ - {text: 'Overview', link: '/admin/extending/overview'}, - {text: 'Access Control', link: '/admin/extending/access-control'}, - {text: 'Add-ons', link: '/admin/extending/addons'}, - {text: 'Attributes', link: '/admin/extending/attributes'}, - {text: 'Panel', link: '/admin/extending/panel'}, - {text: 'Pages', link: '/admin/extending/pages'}, - {text: 'Resources', link: '/admin/extending/resources'}, - {text: 'Relation Managers', link: '/admin/extending/relation-managers'}, - {text: 'Order Management', link: '/admin/extending/order-management'} - ] - } - ], - - }, - - socialLinks: [ - {icon: 'github', link: 'https://github.com/lunarphp/lunar'}, - {icon: 'twitter', link: 'https://twitter.com/lunarphp'}, - {icon: 'discord', link: 'https://discord.gg/v6qVWaf'}, - ], - - algolia: { - appId: 'ZHX0K72823', - apiKey: '42f3d86ed75f289e5cb75e9d7c6f43f9', - indexName: 'lunarphp' - }, - } -}) diff --git a/docs/.vitepress/theme/Layout.vue b/docs/.vitepress/theme/Layout.vue deleted file mode 100644 index 350febec01..0000000000 --- a/docs/.vitepress/theme/Layout.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - -I'm a description!
" - } - ] -} -``` - -If you need to just get the value for one field, you can use the `translateAttribute` method on the model: - -```php -// Leather boots -$product->translateAttribute('name'); - -// Bootes en cuires -$product->translateAttribute('name', 'fr'); - -// Leather boots -$product->translateAttribute('name', 'FOO'); -// We will default here to either the current system locale or the first value available. -``` - -### Advanced usage - -```php -use Lunar\Base\Traits\HasAttributes; - -class ProductType extends Model -{ - use HasAttributes; - - //... -} - -``` - -```php - -use Lunar\Base\Casts\AsAttributeData; - -class Product extends Model -{ - /** - * Define which attributes should be cast. - * - * @var array - */ - protected $casts = [ - 'attribute_data' => AsAttributeData::class, - ]; - - //... -} - -``` - -```php - -use Lunar\Base\Casts\AsAttributeData; - -class ProductVariant extends Model -{ - /** - * Define which attributes should be cast. - * - * @var array - */ - protected $casts = [ - 'attribute_data' => AsAttributeData::class, - ]; - - //... -} - -``` - - -## Attribute Groups - -Attribute Groups form a collection of attributes that are logically grouped together for display purposes. - -A good example might be an "SEO" attribute group which has attributes for "Meta Title" and "Meta Description". - -```php -Lunar\Models\AttributeGroup -``` - -|Field|Description| -|:-|:-| -|`name`|Laravel Collection of translations `{'en': 'SEO'}`| -|`handle`|Kebab-cased reference, e.g. `seo`| -|`position`|An integer used to define the sorting order of groups| diff --git a/docs/core/reference/carts.md b/docs/core/reference/carts.md deleted file mode 100644 index 8db0f047db..0000000000 --- a/docs/core/reference/carts.md +++ /dev/null @@ -1,521 +0,0 @@ -# Carts - -## Overview - -Carts are a collection of products (or other custom purchasable types) that you -would like to order. Carts belong to Users (which relate to Customers). - -::: tip -Cart prices are dynamically calculated and are not stored (unlike Orders). -::: - -## Carts - -```php -Lunar\Models\Cart -``` - -| Field | Description | -|:------------|:--------------------------------------------------------------------------------| -| id | Unique ID for the cart. | -| user_id | Can be `null` for guest users. | -| customer_id | Can be `null`. | -| merged_id | If a cart was merged with another cart, it defines the cart it was merged into. | -| currency_id | Carts can only be for a single currency. | -| channel_id | | -| coupon_code | Can be `null`, stores a promotional coupon code, e.g. `SALE20`. | -| created_at | | -| updated_at | When an order was created from the basket, via a checkout. | -| meta | JSON data for saving any custom information. | - -### Creating a cart - -```php -$cart = \Lunar\Models\Cart::create([ - 'currency_id' => 1, - 'channel_id' => 2, -]); -``` - -## Cart Lines - -```php -Lunar\Models\CartLine -``` - -| Field | Description | -|:-----------------|:---------------------------------------------| -| id | | -| cart_id | | -| purchasable_type | e.g. `product_variant` | -| purchasable_id | | -| quantity | | -| created_at | | -| updated_at | | -| meta | JSON data for saving any custom information. | - -```php -$purchasable = \Lunar\Models\ProductVariant::create([/** ... */]); -$cartLine = new \Lunar\Models\CartLine([ - 'cart_id' => 1, - 'purchasable_type' => $purchasable->getMorphClass(), - 'purchasable_id' => $purchasable->id, - 'quantity' => 2, - 'meta' => [ - 'personalization' => 'Love you mum xxx', - ] -]); - -// Or you can use the relationship on the cart. -$cart->lines()->create([/* .. */]); -``` - -### Validation - -When adding items to a cart there are a series of validation actions which are run, which are defined in the `config/lunar/cart.php` config file. - -These actions will throw a `Lunar\Exceptions\Carts\CartException`. - -```php -try { - $cart->add($purchasable, 500); -} catch (\Lunar\Exceptions\Carts\CartException $e) { - $error = $e->getMessage(); -} -``` - -Now you have a basic Cart up and running, it's time to show you how you would -use the cart to get all the calculated totals and tax. - -We've also tried to make Carts extendable as much as possible so, depending on -what your stores requirements are, you are free to chop and change things as -much as you need to. - -## Hydrating the cart totals - -```php -$cart->calculate(); -``` - -This will create a "hydrated" version of your cart with the following: - -::: tip -All values will return a `Lunar\Datatypes\Price` object. So you have -access to the following: `value`, `formatted`, `decimal` -::: - -```php -$cart->total; // The total price value for the cart -$cart->subTotal; // The cart sub total, excluding tax -$cart->subTotalDiscounted; // The cart sub total, minus the discount amount. -$cart->shippingTotal; // The monetary value for the shipping total. (if applicable) -$cart->taxTotal; // The monetary value for the amount of tax applied. -$cart->taxBreakdown; // This is a collection of all taxes applied across all lines. -$cart->discountTotal; // The monetary value for the discount total. -$cart->discountBreakdown; // This is a collection of how discounts were calculated -$cart->shippingSubTotal; // The shipping total, excluding tax. -$cart->shippingTotal; // The shipping total including tax. -$cart->shippingBreakdown; // This is a collection of the shipping breakdown for the cart. - -foreach ($cart->taxBreakdown as $taxRate) { - $taxRate->name - $taxRate->total->value -} - -foreach ($cart->shippingBreakdown->items as $shippingBreakdown) { - $shippingBreakdown->name; - $shippingBreakdown->identifier; - $shippingBreakdown->price->formatted(); -} - - -foreach ($cart->discountBreakdown as $discountBreakdown) { - $discountBreakdown->discount_id - foreach ($discountBreakdown->lines as $discountLine) { - $discountLine->quantity - $discountLine->line - } - $discountBreakdown->total->value -} - -foreach ($cart->lines as $cartLine) { - $cartLine->unitPrice; // The monetary value for a single item. - $cartLine->unitPriceInclTax; // The monetary value for a single item, including tax amount. - $cartLine->total; // The total price value for the cart - $cartLine->subTotal; // The sub total, excluding tax - $cartLine->subTotalDiscounted; // The sub total, minus the discount amount. - $cartLine->taxAmount; // The monetary value for the amount of tax applied. - $cartLine->taxBreakdown; // This is a collection of all taxes applied across all lines. - $cartLine->discountTotal; // The monetary value for the discount total. -} - -``` - -## Modifying Carts - -If you need to programmatically change the Cart values, e.g. custom discounts or -prices, you will want to extend the Cart. - -You can find out more in the Extending Lunar section for -[Cart Extending](/core/extending/carts). - -## Calculating Tax - -During the cart's lifetime, it's unlikely you will have access to any address -information, which can be a pain when you want to accurately display the amount -of tax applied to the basket. Moreover, some countries don't even show tax until -they reach the checkout. We've tried to make this as easy and extendable as -possible for you as the developer to build your store. - -When you calculate the cart totals, you will be able to set the billing and/or -shipping address on the cart, which will then be used when we calculate which -tax breakdowns should be applied. - -```php -$shippingAddress = [ - 'country_id' => null, - 'title' => null, - 'first_name' => null, - 'last_name' => null, - 'company_name' => null, - 'tax_identifier' => null, - 'line_one' => null, - 'line_two' => null, - 'line_three' => null, - 'city' => null, - 'state' => null, - 'postcode' => 'H0H 0H0', - 'delivery_instructions' => null, - 'contact_email' => null, - 'contact_phone' => null, - 'meta' => null, -]; - -$billingAddress = /** .. */; - -$cart->setShippingAddress($shippingAddress); -$cart->setBillingAddress($billingAddress); -``` - -You can also pass through a `\Lunar\Models\Address` model, or even another -`\Lunar\Models\CartAddress` - -```php -$shippingAddress = \Lunar\Models\Address::first(); - -$cart->setShippingAddress($shippingAddress); - -$cart->setBillingAddress( - $cart->shippingAddress -); -``` - -## Cart Session Manager - -::: tip -The cart session manager is useful if you're building a traditional -Laravel storefront which makes use of sessions. -::: - -When building a store, you're going to want an easy way to fetch the cart for -the current user (or guest user) by retrieving it from their current session. -Lunar provides an easy to use class to make this easier for you, so you don't -have to keep reinventing the wheel. - -### Available config - -Configuration for your cart is handled in `lunar/cart.php` - -| Field | Description | Default | -|:--------------|:---------------------------------------------------------------------------------------|:-------------| -| `auth_policy` | When a user logs in, how should we handle merging of the basket? | `merge` | -| `eager_load` | Which relationships should be eager loaded by default when calculating the cart totals | - -There is additional, separate, config specifically for when using the `CartSession` located in `lunar/cart_session.php`. - -| Field | Description | Default | -|:---------------------------------|:-------------------------------------------------------------------|:-------------| -| `session_key` | What key to use when storing the cart id in the session | `lunar_cart` | -| `auto_create` | If no current basket exists, should we create one in the database? | `false` | -| `allow_multiple_orders_per_cart` | Whether carts can have multiple orders associated to them. | `false` | - -### Getting the cart session instance - -You can either use the facade or inject the `CartSession` into your code. - -```php -$cart = \Lunar\Facades\CartSession::current(); - -public function __construct( - protected \Lunar\Base\CartSessionInterface $cartSession -) { - // ... -} -``` - -### Fetching the current cart - -```php -$cart = \Lunar\Facades\CartSession::current(); -``` - -When you call current, you have two options, you either return `null` if they -don't have a cart, or you want to create one straight away. By default, we do -not create them initially as this could lead to a ton of cart models being -created for no good reason. If you want to enable this functionality, you can -adjust the config in `lunar/cart_session.php` - -### Forgetting the cart -Forgetting the cart will remove it from the user session and also soft-delete -the cart in the database. - -```php -CartSession::forget(); -``` - -If you don't want to delete the cart, you can pass the following parameter. - -```php -CartSession::forget(delete: false); -``` - - -### Using a specific cart - -You may want to manually specify which cart should be used for the session. - -```php -$cart = \Lunar\Models\Cart::first(); -CartSessionManager::use($cart); -``` - -The other available methods are as follows: - -### Add a cart line - -```php -CartSession::add($purchasable, $quantity); -``` - -### Add multiple lines - -```php -CartSession::addLines([ - [ - 'purchasable' => \Lunar\Models\ProductVariant::find(123), - 'quantity' => 25, - 'meta' => ['foo' => 'bar'], - ], - // ... -]); -``` - -_Accepts a `collection` or an `array`_ - -### Update a single line - -```php -CartSession::updateLine($cartLineId, $quantity, $meta); -``` - -### Update multiple lines - -```php -CartSession::updateLines(collect([ - [ - 'id' => 1, - 'quantity' => 25, - 'meta' => ['foo' => 'bar'], - ], - // ... -])); -``` - -### Remove a line - -```php -CartSession::remove($cartLineId); -``` - -### Clear a cart - -This will remove all lines from the cart. - -```php -CartSession::clear(); -``` - -### Associating a cart to a user - -You can easily associate a cart to a user. - -```php -CartSession::associate($user, 'merge'); -``` - -### Associating a cart to a customer - -You can easily associate a cart to a customer. - -```php -CartSession::setCustomer($customer); -``` - - -## Adding shipping/billing address - -As outlined above, you can add shipping / billing addresses to the cart using -the following methods: - -```php -$cart->setShippingAddress([ - 'first_name' => null, - 'last_name' => null, - 'line_one' => null, - 'line_two' => null, - 'line_three' => null, - 'city' => null, - 'state' => null, - 'postcode' => null, - 'country_id' => null, -]); -$cart->setBillingAddress([ - 'first_name' => null, - 'last_name' => null, - 'line_one' => null, - 'line_two' => null, - 'line_three' => null, - 'city' => null, - 'state' => null, - 'postcode' => null, - 'country_id' => null, -]); -``` - -You can easily retrieve these addresses by accessing the appropriate property: - -```php -$cart->shippingAddress; -$cart->billingAddress; -``` - -### ShippingOption override - -In some cases you might want to present an estimated shipping cost without users having to fill out a full shipping address, this is where the `ShippingOptionOverride` comes in, if set on the cart it can be used to calculate shipping for a single request. - -```php -$shippingOption = $cart->getEstimatedShipping([ - 'postcode' => '123456', - 'state' => 'Essex', - 'country' => Country::first(), -]); -```` - -This will return an estimated (cheapest) shipping option for the cart, based on it's current totals. By default this will not be taken into account when calculating shipping in the cart pipelines, in order to enable that we need to pass an extra parameter. - -```php -$shippingOption = $cart->getEstimatedShipping([ - 'postcode' => '123456', - 'state' => 'Essex', - 'country' => Country::first(), -], setOverride: true); -```` - -Now when the pipelines are run, the option which was returned by `getEstimatedShipping` will be used when calculating shipping totals, bypassing any other logic, note this will only happen for that one request. - -If you are using the `CartSession` manager, you can easily set the parameters you want to estimate shipping so you don't need to pass them each time: - -```php -CartSession::estimateShippingUsing([ - 'postcode' => '123456', - 'state' => 'Essex', - 'country' => Country::first(), -]); -``` - -You can also manually set the shipping method override directly on the cart. - -```php -$cart->shippingOptionOverride = new \Lunar\DataTypes\ShippingOption(/* .. */); -``` - -Calling `CartSession::current()` by itself won't trigger the shipping override, but you can pass the `estimateShipping` parameter to enable it: - -```php -// Will not use the shipping override, default behaviour. -CartSession::current(); - -// Will use the shipping override, based on what is set using `estimateShippingUsing` -CartSession::current(estimateShipping: true); -``` - -## Handling User Login - -When a user logs in, you will likely want to check if they have a cart -associated to their account and use that, or if they have started a cart as a -guest and logged in, you will likely want to be able to handle this. Lunar takes -the pain out of this by listening to the authentication events and responding -automatically by associating any previous guest cart they may have had and, -depending on your `auth_policy` merge or override the basket on their account. - -## Determining cart changes - -Carts by nature are dynamic, which means anything can change at any moment. This means it can be quite challenging to -determine whether a cart has changed from the one currently loaded, for example, if the user goes to check out and -changes their cart on another tab, how does the checkout know there has been a change? - -To help this, a cart will have a fingerprint generated which you can check to determine whether there has been -any changes and if so, refresh the cart. - -```php -$cart->fingerprint(); - -try { - $cart->checkFingerprint('4dfAW33awd'); -} catch (\Lunar\Exceptions\FingerprintMismatchException $e) { - //... Refresh the cart. -} -``` - -### Changing the underlying class. - -The class which generates the fingerprint is referenced in `config/lunar/cart.php`. - -```php -return [ - // ... - 'fingerprint_generator' => Lunar\Actions\Carts\GenerateFingerprint::class, -]; -``` - -In most cases you won't need to change this. - -## Pruning cart data - -Over time you will experience a build up of carts in your database that you may want to regularly remove. - -You can enable automatic removal of these carts using the `lunar.carts.prune_tables.enabled` config. By setting this to `true` any carts without an order associated will be removed after 90 days. - -You can change the number of days carts are retained for using the `lunar.carts.prune_tables. prune_interval` config. - -If you have specific needs around pruning you can also change the `lunar.carts.prune_tables.pipelines` array to determine what carts should be removed. - - - -```php -return [ - // ... - 'prune_tables' => [ - - 'enabled' => false, - - 'pipelines' => [ - Lunar\Pipelines\CartPrune\PruneAfter::class, - Lunar\Pipelines\CartPrune\WithoutOrders::class, - ], - - 'prune_interval' => 90, // days - - ], -]; -``` diff --git a/docs/core/reference/channels.md b/docs/core/reference/channels.md deleted file mode 100644 index 827b58d8cc..0000000000 --- a/docs/core/reference/channels.md +++ /dev/null @@ -1,55 +0,0 @@ -# Channels - -## Overview - -Default `webstore`. - -## Assigning channels to models. - -You can assign Eloquent models to different channels and specify whether they are enabled permanently or whether they should be scheduled to be enabled. - -In order to add this kind of functionality to your model, you need to add the `HasChannels` trait. - -```php -scheduleChannel($channel, now()->addDays(14), now()->addDays(24)); - -// Schedule the product to be enabled straight away -$product->scheduleChannel($channel); - -// The schedule method will accept and array or collection of channels. -$product->scheduleChannel(Channel::get()); -``` - -There is also a channel scope available to models which use this trait: - -```php -// Limit to a single channel -Product::channel($channel)->get(); - -// Limit to multiple channels -Product::channel([$channelA, $channelB])->get(); - -// Limit to a channel available the next day -Product::channel($channelA, now()->addDay())->get(); - -// Limit to a channel within a date range. -Product::channel($channelA, now()->addDay(), now()->addDays(2))->get(); -``` diff --git a/docs/core/reference/collections.md b/docs/core/reference/collections.md deleted file mode 100644 index 940898254a..0000000000 --- a/docs/core/reference/collections.md +++ /dev/null @@ -1,92 +0,0 @@ -# Collections - -## Overview - -Collections, although not strictly the same, are akin to Categories. They serve to allow you to add products, either explicitly or via certain criteria, for use on your store. - -For example, you may have a Collection called "Red T-Shirts" and within that collection specify that any product which has the tag "Red" and "T-Shirt" to be included. - -A collection can also have other collections underneath it, forming a nested set hierarchy. - -A collection must also belong to a collection group, this allows you to have greater flexibility when building out things like Menus and Landing pages. - - -## Collection Groups - -Create a collection group - -```php -$group = \Lunar\Models\CollectionGroup::create([ - 'name' => 'Main Catalogue', - 'handle' => 'main-catalogue' // Will auto generate if omitted. -]); -``` - - -## Collections - -Collections are a hierarchy of models that have products associated to them, you can think of them as "Categories". Once you have added products you can then determine how they are sorted on display. - -### Create a collection - -```php -$collection = \Lunar\Models\Collection::create([ - 'attribute_data' => [ - 'name' => new \Lunar\FieldTypes\Text('Clearance'), - ], - 'collection_group_id' => $group->id, -]); -``` - - -### Add a child collection - -```php -$child = new \Lunar\Models\Collection::create([/*..*/]); - -$collection->appendNode($child); -``` - -This results in the following - -```bash -- Clearance - - Child -``` - -Lunar uses the [Laravel Nested Set](https://github.com/lazychaser/laravel-nestedset) package, so feel free to take a look at it to see what's possible. - -### Adding products - -Products are related using a `BelongsToMany` relationship with a pivot column for `position`. - -```php -$products = [ - 1 => [ - 'position' => 1, - ], - 2 => [ - 'position' => 2, - ] -]; - -$collection->products()->sync($products); -``` - -::: tip -The key in the `$products` array is the product id -::: - -### Sorting products - -Lunar comes with a handful of criteria out the box for sorting products in a collection: - -|Name|Description| -|:-|:-| -|`min_price:asc`|Sorts using the base price ascending| -|`min_price:desc`|Sorts using the base price descending| -|`sku:asc`|Sorts using the sku ascending| -|`sku:desc`|Sorts using the sku descending| -|`custom`|This will allow you to specify the order of each product manually| - -Depending on what you have as the sort time on the collection, Lunar will automatically sort the products for you when you update the products. diff --git a/docs/core/reference/currencies.md b/docs/core/reference/currencies.md deleted file mode 100644 index 0100c3aaf5..0000000000 --- a/docs/core/reference/currencies.md +++ /dev/null @@ -1,47 +0,0 @@ -# Currencies - -## Overview - -Currencies allow you to charge different amounts relative to the currency you're targeting. - -## Creating a currency - -```php -\Lunar\Models\Currency::create([ - 'code' => 'GBP', - 'name' => 'British Pound', - 'exchange_rate' => 1.0000, - 'decimal_places' => 2, - 'enabled' => 1, - 'default' => 1, -]); -``` - -|Field|Description| -|:-|:-| -|`code`|This should be the `ISO 4217` currency code. | -|`name`|A given name for the currency.| -|`exchange_rate`|This should be the exchange rate relative to the default currency (see below)| -|`decimal_places`|Specify the decimal places, e.g. 2| -|`enabled`|Whether the currency is enabled| -|`default`|Whether the currency is the default| - -## Exchange rates -These are relative to the default currency. For example assuming we have the following: - -```php -\Lunar\Models\Currency::create([ - 'code' => 'GBP', - 'name' => 'British Pound', - 'exchange_rate' => 1.0000, - 'decimal_places' => 2, - 'enabled' => 1, - 'default' => 1, -]); -``` - -We set the model to be the default and we set the exchange rate to be `1` as we're defining this as our base (default) currency. - -Now, say we wanted to add EUR (Euros). Currently the exchange rate from GBP to EUR is `1.17`. But we want this to be relative to our default record. So 1 / 1.17 = 0.8547. - -It's entirely up to you what you want to set the exchange rates as, it is also worth mentioning that this is independent of product pricing in the sense that you can specify the price per currency. The exchange rate serves as a helper when working with prices so you have something to go by. diff --git a/docs/core/reference/customers.md b/docs/core/reference/customers.md deleted file mode 100644 index 97885f9e60..0000000000 --- a/docs/core/reference/customers.md +++ /dev/null @@ -1,271 +0,0 @@ -# Customers - -## Overview - -We use Customers in Lunar to store the customer details, rather than Users. We do this for a few reasons. One, so that we leave your User models well alone and two, because it provides flexibility. - -## Customers - -```php -Lunar\Models\Customer -``` - -|Field|Description| -|:-|:-| -|`id`|| -|`title`|Mr, Mrs, Miss, etc| -|`first_name`|| -|`last_name`|| -|`company_name`|nullable| -|`tax_identifier`|nullable| -|`account_ref`|nullable| -|`attribute_data`|JSON| -|`meta`|JSON| -|`created_at`|| -|`updated_at`|| - -### Creating a customer - -```php -Lunar\Models\Customer::create([ - 'title' => 'Mr.', - 'first_name' => 'Tony', - 'last_name' => 'Stark', - 'company_name' => 'Stark Enterprises', - 'tax_identifier' => null, - 'meta' => [ - 'account_no' => 'TNYSTRK1234' - ], -]) -``` - -### Relationships - -- Customer Groups `customer_customer_group` -- Users - `customer_user` - -## Users - -Customers will typically be associated with a user, so they can place orders. But it is also possible to have multiple users associated with a customer. This can be useful on B2B e-commerce where a customer may have multiple buyers. - -### Attaching users to a customer - -```php -$customer = \Lunar\Models\Customer::create([/* ... */]); - -$customer->users()->attach($user); - -$customer->users()->sync([1,2,3]); -``` - -## Attaching a customer to a customer group - -```php -$customer = \Lunar\Models\Customer::create([/* ... */]); - -$customer->customerGroups()->attach($customerGroup); - -$customer->customerGroups()->sync([4,5,6]); -``` - -## Impersonating users - -When a customer needs help with their account, it's useful to be able to log in as that user so you can help diagnose the issue they're having. -Lunar allows you to specify your own method of how you want to impersonate users, usually this is in the form of a signed URL an admin can go to in order to log in as the user. - -### Creating the impersonate class - -```php -addMinutes(5), [ - 'user' => $authenticatable->getAuthIdentifier(), - ]); - } -} -``` - -Then you need to register this in `config/lunar-hub/customers.php`. - -```php -return [ - 'impersonate' => App\Auth\Impersonate::class, - // ... -]; -``` - -Once added you will see an option to impersonate the user when viewing a customer. This will then go to the URL specified in your class where you will be able to handle the impersonation logic. - -## Customer Groups - -Default `retail` - -Customer groups allow you to group your customers into logical segments which enables you to define different criteria on models based on what customer belongs to that group. - -These criteria include things like: - -### Pricing - -Specify different pricing per customer group, for example you may have certain prices for customers that are in the `trade` customer group. - -### Product Availability - -You can turn product visibility off depending on the customer group, this would mean only certain products would show depending on the group they belong to. This will also include scheduling availability so you can release products earlier or later to different groups. - ---- -You must have at least one customer group in your store and when you install Lunar you will be given a default one to get you started named `retail`. - -## Creating a customer group - -```php -$customerGroup = Lunar\Models\CustomerGroup::create([ - 'name' => 'Retail', - 'handle' => 'retail', // Must be unique - 'default' => false, -]); -``` - -::: tip -You can only have one default at a time, if you create a customer group and pass default to true, then the existing default will be set to `false`. -::: - -## Scheduling availability - -If you would like to add customer group availability to your own models, you can use the `HasCustomerGroups` trait. - -```php - -// ... -use Lunar\Base\Traits\HasCustomerGroups; - -class MyModel extends Model -{ - use HasCustomerGroups; -} -``` - -You will need to define the relationship for customer groups so that Lunar knows how to handle it. - -```php -public function customerGroups() -{ - return $this->hasMany(\Lunar\Models\CustomerGroup::class)->withTimestamps()->withPivot([/* .. */]); -} -``` -You will then have access to the following methods: - -### Scheduling customer groups - -```php -// Will schedule for this product to be enabled in 14 days for this customer group. -$myModel->scheduleCustomerGroup( - $customerGroup, - $startDate, - $endData, - $pivotData -); - -// Schedule the product to be enabled straight away -$myModel->scheduleCustomerGroup($customerGroup); - -// The schedule method will accept an array or collection of customer groups. -$myModel->scheduleCustomerGroup(CustomerGroup::get()); -``` - -### Unscheduling customer groups - -If you do not want a model to be enabled for a customer group, you can unschedule it. This will keep any previous `start` and `end` dates but will toggle the `enabled` column. - -```php -$myModel->unscheduleCustomerGroup($customerGroup, $pivotData); -``` - -### Parameters - -|Field|Description|Type|Default| -|:-|:-|:-|:-| -|`customerGroup`|A collection of CustomerGroup models or id's.|mixed| -|`startDate`|The date the customer group will be active from.|`DateTime`| -|`endDate`|The date the customer group will be active until.|`DateTime`| -|`pivotData`|Any additional pivot data you may have on your link table. (not including scheduling defaults)|`array`| - - -**Pivot Data** - -By default the following values are used for `$pivotData` - -- `enabled` - Whether the customer group is enabled, defaults to `true` when scheduling and `false` when unscheduling. - -You can override any of these yourself as they are merged behind the scenes. - -### Retrieving the relationship - -The `HasCustomerGroup` trait adds a `customerGroup` scope to the model. This lets you query based on availability for a specific or multiple customer groups. - -The scope will accept either a single ID or instance of `CustomerGroup` and will accept an array. - -```php -$results = MyModel::customerGroup(1, $startDate, $endDate)->paginate(); - -$results = MyModel::customerGroup([ - $groupA, - $groupB, -])->paginate(50); -``` - -The start and end dates should be `DateTime` objects with will query for the existence of a customer group association with the start and end dates between those given. These are optional and the following happens in certain situations: - -**Pass neither `startDate` or `endDate`** - -Will query for customer groups which are enabled and the `startDate` is after `now()` - -**Pass only `startDate`** - -Will query for customer groups which are enabled, the start date is after the given date and the end date is either null or before `now()` - -**Pass both `startDate` and `endDate`** - -Will query for customer groups which are enabled, the start date is after the given date and the end date is before the given date. - -**Pass `endDate` without `startDate`** - -Will query for customer groups which are enabled, the start date is after the `now()` and the end date is before the given date. - -If you omit the second parameter the scope will take the current date and time. - -::: tip -A model will only be returned if the `enabled` column is positive, regardless of whether the start and end dates match. -::: - -### Limit by customer group - -Eloquent models which use the `HasCustomerGroups` trait have a useful scope available: - -```php -// Limit products available to a single customer group -Product::customerGroup($customerGroup)->get(); - -// Limit products available to multiple customer groups -Product::customerGroup([$groupA, $groupB])->get(); - -// Limit to products which are available the next day -Product::customerGroup($groupA, now()->addDay())->get(); - -// Limit to products which are available within a date range. -Product::customerGroup($groupA, now()->addDay(), now()->addDays(2))->get(); -``` diff --git a/docs/core/reference/discounts.md b/docs/core/reference/discounts.md deleted file mode 100644 index ab2756891f..0000000000 --- a/docs/core/reference/discounts.md +++ /dev/null @@ -1,143 +0,0 @@ -# Discounts - -## Overview - -// ... - -## Discounts - -```php -Lunar\Models\Discount -``` - -| Field | Description | Example | -|:-------------|:-------------------------------------------------------------|:--------------------------------------| -| `id` | | | -| `name` | The given name for the discount | | -| `handle` | The unique handle for the discount | | -| `type` | The type of discount | `Lunar\DiscountTypes\BuyXGetY` | -| `data` | JSON | Any data to be used by the type class | -| `starts_at` | The datetime the discount starts (required) | | -| `ends_at` | The datetime the discount expires, if `NULL` it won't expire | | -| `uses` | How many uses the discount has had | | -| `max_uses` | The maximum times this discount can be applied storewide | | -| `priority` | The order of priority | | -| `stop` | Whether this discount will stop others after propagating | | -| `created_at` | | | -| `updated_at` | | | - -### Creating a discount - -```php -Lunar\Models\Discount::create([ - 'name' => '20% Coupon', - 'handle' => '20_coupon', - 'type' => 'Lunar\DiscountTypes\Coupon', - 'data' => [ - 'coupon' => '20OFF', - 'min_prices' => [ - 'USD' => 2000 // $20 - ], - ], - 'starts_at' => '2022-06-17 13:30:55', - 'ends_at' => null, - 'max_uses' => null, -]) -``` - -### Fetching a discount - -The following scopes are available: - -```php -/** -* Query for discounts using the `start_at` and `end_at` dates. - */ -Discount::active()->get(); - -/** -* Query for discounts where the `uses` column is less than the `max_uses` column or `max_uses` is null. - */ -Discount::usable()->get(); - -/** -* Query for discounts where the associated products are of a certain type, based on given product ids. - */ -Discount::products($productIds, $type = 'condition'); -``` - -### Resetting the discount cache - -For performance reasons the applicable discounts are cached per request. If you need to reset this cache (for example after adding a discount code) you should call `resetDiscounts()`: - -```php -Discount::resetDiscounts(); -``` - -## Discountables - -You can relate a purchasable, collection or variant to a discount via this model. Each has a type for whether it's a `condition` or `reward`. - -- `condition` - If your discount requires these models to be in the cart to activate -- `reward` - Once the conditions are met, discount one of more of these models. - -```php -Lunar\Models\Discountable -``` - -| Field | Description | Example | -|:--------------------|:------------------------|:------------------------------| -| `id` | | | -| `discount_id` | | | -| `discountable_type` | | `product_variant` | -| `discountable_id` | | | -| `type` | `condition` or `reward` | | -| `created_at` | | | -| `updated_at` | | | - -### Relationships - -- Discountable `discountables` -- Users - `customer_user` - -### Adding your own Discount type - -```php -addMedia($request->file('image'))->toMediaCollection('images'); -``` - -For more information on what's available, see [Associating files](https://spatie.be/docs/laravel-medialibrary/v9/basic-usage/associating-files) - -## Fetching images - -```php -$product = \Lunar\Models\Product::find(123); - -$product->getMedia('images'); -``` -For more information on what's available, see [Retrieving media](https://spatie.be/docs/laravel-medialibrary/v9/basic-usage/retrieving-media) - -## Fallback images -If your model does not contain any images, calling getFirstMediaUrl or getFirstMediaPath will return null. You can -provide a fallback path/url in the config `lunar/media` or `.env`. -```php -'fallback' => [ - 'url' => env('FALLBACK_IMAGE_URL', null), - 'path' => env('FALLBACK_IMAGE_PATH', null) -] -``` - -## Media Collections & Conversions - -Lunar provides a way to customise the media collections and conversions for each model that implements the `HasMedia` -trait. - -You will find the default settings in the config `lunar/media`. Here you can switch out the class that handles the -registration of the media definitions. - -### Custom Media Definitions - -To create custom media definitions you will want to create your own implementation along the lines of the code below. - -When registering media definitions you can define not only the name but many other options. -See [Spatie Media Library](https://spatie.be/docs/laravel-medialibrary/v10/working-with-media-collections/defining-media-collections) for more information. - -```php -use Lunar\Base\MediaDefinitionsInterface; -use Spatie\Image\Enums\Fit; -use Spatie\MediaLibrary\HasMedia; -use Spatie\MediaLibrary\MediaCollections\MediaCollection; -use Spatie\MediaLibrary\MediaCollections\Models\Media; - -class CustomMediaDefinitions implements MediaDefinitionsInterface -{ - public function registerMediaConversions(HasMedia $model, Media $media = null): void - { - // Add a conversion for the admin panel to use - $model->addMediaConversion('small') - ->fit(Fit::Fill, 300, 300) - ->sharpen(10) - ->keepOriginalImageFormat(); - } - - public function registerMediaCollections(HasMedia $model): void - { - $fallbackUrl = config('lunar.media.fallback.url'); - $fallbackPath = config('lunar.media.fallback.path'); - - // Reset to avoid duplication - $model->mediaCollections = []; - - $collection = $model->addMediaCollection('images'); - - if ($fallbackUrl) { - $collection = $collection->useFallbackUrl($fallbackUrl); - } - - if ($fallbackPath) { - $collection = $collection->useFallbackPath($fallbackPath); - } - - $this->registerCollectionConversions($collection, $model); - } - - protected function registerCollectionConversions(MediaCollection $collection, HasMedia $model): void - { - $conversions = [ - 'zoom' => [ - 'width' => 500, - 'height' => 500, - ], - 'large' => [ - 'width' => 800, - 'height' => 800, - ], - 'medium' => [ - 'width' => 500, - 'height' => 500, - ], - ]; - - $collection->registerMediaConversions(function (Media $media) use ($model, $conversions) { - foreach ($conversions as $key => $conversion) { - $model->addMediaConversion($key) - ->fit( - Fit::Fill, - $conversion['width'], - $conversion['height'] - )->keepOriginalImageFormat(); - } - }); - } - - public function getMediaCollectionTitles(): array - { - return [ - 'images' => 'Images', - ]; - } - - public function getMediaCollectionDescriptions(): array - { - return [ - 'images' => '', - ]; - } -} -``` -Then register your new class against the model(s) you wish to use it. - -```php -return [ - - 'definitions' => [ - Lunar\Models\Product::class => CustomMediaDefinitions::class, - //.. - ], - - //.. -``` - -#### Generate Media Conversions - -To regenerate conversions, e.g. if you have changed the configuration, you can run the following command. - -```sh -php artisan media-library:regenerate -``` - -This will create queue jobs for each media entry to be re-processed. More information can be found on the -[medialibrary website](https://spatie.be/docs/laravel-medialibrary/v9/converting-images/regenerating-images) - - -## Extending - -You can extend your own models to use media, either by using our implementation or by implementing medialibrary -directly. It's totally up to you and your requirements. If you want to use medialibrary directly, -[just follow their guides](https://spatie.be/docs/laravel-medialibrary/v9/basic-usage/preparing-your-model) and you'll be all set. - -::: warning -If you decide to use medialibrary directly, you will not have access to our transformations or any other Lunar features -we add. -::: - -### Extending with Lunar - -To enable image transformations on your models within Lunar, simply add the `HasMedia` trait. - -```php -createOrder( - allowMultipleOrders: false, - orderIdToUpdate: null, -); -``` - -- `allowMultipleOrders` - Generally carts will only have one draft order associated, however if you want to allow carts to - have multiple, you can pass `true` here. -- `orderIdToUpdate` - You can optionally pass the ID of an order to update instead of attempting to create a new order, this must be a draft order i.e. a null `placed_at` and related to the cart. - -The underlying class for creating an order is `Lunar\Actions\Carts\CreateOrder`, you are free to override this in the -config file `config/cart.php` - -```php -return [ - // ... - 'actions' => [ - // ... - 'order_create' => CustomCreateOrder::class - ] -] -``` - -At minimum your class should look like the following: - -```php -final class CreateOrder extends Lunar\Actions\AbstractAction -{ - /** - * Execute the action. - */ - public function execute( - Cart $cart, - bool $allowMultipleOrders = false, - int $orderIdToUpdate = null - ): self { - return $this; - } -} -``` - -### Validating a cart before creation. - -If you also want to check before you attempt this if the cart is ready to create an order, you can call the helper -method: - -```php -$cart->canCreateOrder(); -``` - -Under the hood this will use the `ValidateCartForOrderCreation` class which lunar provides and throw any validation -exceptions with helpful messages if the cart isn't ready to create an order. - -You can specify your own class to handle this in `config/cart.php`. - -```php -return [ - // ... - 'validators' => [ - 'order_create' => MyCustomValidator::class, - ] -] -``` - -Which may look something like: - -```php -parameters['cart']; - - if ($somethingWentWrong) { - return $this->fail('There was an issue'); - } - - return $this->pass(); - } -} -``` - -## Order Reference Generation - -By default Lunar will generate a new order reference for you when you create an order from a cart. The format for this -is: - -``` -{prefix?}{0..0}{orderId} -``` - -`{0..0}` indicates the order id will be padded until the length is 8 digits (not including the prefix) -The prefix is optional and defined in the `lunar/orders.php` config file, which gives access to a number of settings you can change. - -### Custom Generators - -If your store has a specific requirement for how references are generated, you can easily swap out the Lunar one for -your own: - -`config/lunar/orders.php` - -```php -return [ - 'reference_generator' => App\Generators\MyCustomGenerator::class, -]; -``` - -Or, if you don't want references at all (not recommended) you can simply set it to `null` - -Here's the underlying class for the custom generator: - -```php -namespace App\Generators; - -use Lunar\Models\Order; - -class MyCustomGenerator implements OrderReferenceGeneratorInterface -{ - /** - * {@inheritDoc} - */ - public function generate(Order $order): string - { - // ... - return 'my-custom-reference'; - } -} -``` - -## Modifying Orders - -If you need to programmatically change the Order values or add in new behaviour, you will want to extend the Order -system. - -You can find out more in the Extending Lunar section for [Order Modifiers](/core/extending/orders). - -## Order Lines - -```php -Lunar\Models\OrderLine -``` - -| Field | Description | -|:-----------------|:-------------------------------------------------------------------------------------------------------------------------------------------------| -| id | | -| order_id | | -| purchasable_type | Morph reference for the purchasable item e.g. `product_variant` | -| purchasable_id | -| type | Whether `digital`,`physical` etc -| description | A description of the line item -| option | If this was a variant, the option info is here -| identifier | Something to identify the purchasable item, usually an `sku` -| unit_price | The unit price of the line -| unit_quantity | The line unit quantity, usually this is 1 -| quantity | The amount of this item purchased -| sub_total | The sub total minus any discounts, excl. tax -| discount_total | Any discount amount excl. tax -| tax_breakdown | A json field for the tax breakdown e.g. `[{"description": "VAT", "identifier" : "vat", "value": 123, "percentage": 20, "currency_code": "GBP"}]` -| tax_total | The total amount of tax applied -| total | The grand total with tax -| notes | Any additional order notes -| meta | Any additional meta info you wish to store -| created_at | | -| updated_at | | - - -### Create an order line - -::: tip -If you are using the `createOrder` method on a cart, this is all handled for you automatically. -::: - -```php -\Lunar\Models\OrderLine::create([ - // ... -]); -``` - -Or via the relationship - -```php -$order->lines()->create([ - // ... -]); -``` - -## Order Addresses - -An order can have many addresses, typically you would just have one for billing and one for shipping. - -::: tip -If you are using the `createOrder` method on a cart, this is all handled for you automatically. -::: - -```php -\Lunar\Models\OrderAddress::create([ - 'order_id' => 1, - 'country_id' => 1, - 'title' => null, - 'first_name' => 'Jacob', - 'last_name' => null, - 'company_name' => null, - 'tax_identifier' => null, - 'line_one' => '123 Foo Street', - 'line_two' => null, - 'line_three' => null, - 'city' => 'London', - 'state' => null, - 'postcode' => 'NW1 1WN', - 'delivery_instructions' => null, - 'contact_email' => null, - 'contact_phone' => null, - 'type' => 'shipping', // billing/shipping - 'shipping_option' => null, // A unique code for you to identify shipping -]); - -// Or via the relationship. -$order->addresses()->create([ - // ... -]); -``` - -You can then use some relationship helpers to fetch the address you need: - -```php -$order->shippingAddress; -$order->billingAddress; -``` - -## Shipping Options - -::: tip -A Shipping Tables addon is planned to make setting up shipping in the admin hub easy for most scenarios. -::: - -To add Shipping Options you will need to [extend Lunar](/core/extending/shipping) to add in your own logic. - -Then in your checkout, or where ever you want, you can fetch these options: - -```php -\Lunar\Facades\ShippingManifest::getOptions(\Lunar\Models\Cart $cart); -``` - -This will return a collection of `Lunar\DataTypes\ShippingOption` objects. - -### Adding the shipping option to the cart - -Once the user has selected the shipping option they want, you will need to add this to the cart so it can calculate the -new totals. - -```php -$cart->setShippingOption(\Lunar\DataTypes\ShippingOption $option); -``` - -## Transactions - -```php -Lunar\Models\Transaction -``` - -| Field | Description | -|:-----------|:----------------------------------------------------------------------------------------------| -| id | | -| success | Whether the transaction was successful | -| refund | `true` if this was a refund | -| driver | The payment driver used e.g. `stripe` | -| amount | An integer amount | -| reference | The reference returned from the payment Provider. Used to identify the transaction with them. -| status | A string representation of the status, unlinked to Lunar e.g. `settled` | -| notes | Any relevant notes for the transaction -| card_type | e.g. `visa` -| last_four | Last 4 digits of the card -| meta | Any additional meta info you wish to store -| created_at | | -| updated_at | | - -### Create a transaction - -::: tip -Just because an order has a transaction does not mean it has been placed. Lunar determines whether an order is -considered placed when the `placed_at` column has a datetime, regardless if any transactions exist or not. -::: - -Most stores will likely want to store a transaction against the order, this helps determining how much has been paid, -how it was paid and give a clue on the best way to issue a refund if needed. - -```php -\Lunar\Models\Transaction::create([ - //... -]); - -// Or via the order -$order->transactions()->create([ - //.. -]); -``` - -These can then be returned via the relationship. - -```php -$order->transactions; // Get all transactions. - -$order->charges; // Get all transactions that are charges. - -$order->refunds; // Get all transactions that are refunds. -``` - -## Payments - -We will be looking to add support for the most popular payment providers, so keep an eye out here as we will list them -all out. - -In the meantime, you can absolutely still get a storefront working, at the end of the day Lunar doesn't really mind what payment provider you use or plan to use. - -In terms of an order, all it's worried about is whether or not the `placed_at` column is populated on the orders table, -the rest is completely up to you how you want to handle that. We have some helper utilities to make such things easier -for you as laid out above. - -And as always, if you have any questions you can reach out on our Discord! - -## Order Status - -The `placed_at` field determines whether an Order is considered draft or placed. The Order model has two helper methods -to determine the status of an Order. - -```php -$order->isDraft(); -$order->isPlaced(); -``` - -## Order Notifications - -Lunar allows you to specify what Laravel mailers/notifications should be available for sending when you update an -order's status. These are configured in the `lunar/orders` config file and are defined like so: - -```php -'statuses' => [ - 'awaiting-payment' => [ - 'label' => 'Awaiting Payment', - 'color' => '#848a8c', - 'mailers' => [ - App\Mail\MyMailer::class, - App\Mail\MyOtherMailer::class, - ], - 'notifications' => [], - ], - // ... -], -``` - -Now when you update an order's status in the hub, you will have these mailers available if the new status -is `awaiting-payment`. You can then choose the email addresses which the email should be sent to and also add an -additional email address if required. - -Once updated, Lunar will keep a render of the email sent out in the activity log so you have a clear history of what's -been sent out. - -:::tip -These email notifications do not get sent out automatically if you update the status outside of the hub. -::: - -### Mailer template - -When building out the template for your mailer, you should assume you have access to the `$order` model. When the status -is updated this is passed through to the view data for the mailer, along with any additional content entered. -Since you may not always have additional content when sending out the mailer, you should check the existence first. - -Here's an example of what the template could look like: - -```blade -Your order with reference {{ $order->reference }} has been dispatched!
- -{{ $order->total->formatted() }}
- -@if($content ?? null) -{{ $content }}
-@endif - -@foreach($order->lines as $line) - -@endforeach -``` - -## Order Invoice PDF - -By default when you click "Download PDF" in the hub when viewing an order, you will get a basic PDF generated for you to -download. You can publish the view that powers this to create your own PDF template. - -```bash -php artisan vendor:publish --tag=lunar.hub.views -``` - -This will create a view called `resources/vendor/lunarpanel/pdf/order.blade.php`, where you will be able to freely -customise the PDF you want displayed on download. diff --git a/docs/core/reference/payments.md b/docs/core/reference/payments.md deleted file mode 100644 index 5141d98fcf..0000000000 --- a/docs/core/reference/payments.md +++ /dev/null @@ -1,356 +0,0 @@ -# Payments - -:::tip -If you're looking for a guide on how to create your own Payment Driver, or for a more in-depth look at how they work, -head over to the [extending section](/core/extending/payments). -::: - -## Overview - -Lunar takes a driver based approach with Payments, meaning you are free to use either add ons to support the provider -you wish to use, or you can create your own to meet your exact needs. - -## Configuration - -All configuration for payments is located in `config/lunar/payments.php`. Here you can specify different types of -payments and the driver each one should use. - -```php - env('PAYMENTS_TYPE', 'offline'), - - 'types' => [ - 'cash-in-hand' => [ - 'driver' => 'offline', - 'released' => 'payment-offline', - ], - 'card' => [ - 'driver' => 'stripe', - 'released' => 'payment-received', - ], - ], -]; -``` - -## Usage - -To use a payment driver, you need to pass the type of payment you wish to use, this will then return an instance of the -driver. - -```php -$driver = \Lunar\Facades\Payments::driver('card'); -``` - -We can then set the cart. - -```php -$driver->cart(\Lunar\Models\Cart $cart); -``` - -Set any additional data that the driver may need. - -```php -$driver->withData([ - 'payment_token' => $token, -]); -``` - -Finally, we can authorize the payment. - -```php -$driver->authorize(); -``` - - diff --git a/docs/core/reference/pricing.md b/docs/core/reference/pricing.md deleted file mode 100644 index db07dfb176..0000000000 --- a/docs/core/reference/pricing.md +++ /dev/null @@ -1,223 +0,0 @@ -# Pricing - -## Overview - -When you display prices on your storefront, you want to be sure the customer is seeing the correct format relative to -the currency they are purchasing in. - -Every storefront is different. We understand as a developer you might want to do this your own way or have very specific -requirements, so we have made price formatting easy to swap out with your own implementation, but also we provide a -suitable default that will suit most use cases. - -## Price formatting - -The class which handles price formatting is referenced in the `config/pricing.php` file: - -```php -return [ - // ... - 'formatter' => \Lunar\Pricing\DefaultPriceFormatter::class, -]; -``` - -When you retrieve a `Lunar\Models\Price` model, you will have access to the `->price` attribute which will return -a `Lunar\DataTypes\Price` object. This is what we will use to get our formatted values. - -The `Lunar\DataTypes\Price` class is not limited to database columns and can be found throughout the Lunar core when -dealing with prices, other examples include: - -### `Lunar\Models\Order` - -- `subTotal` -- `total` -- `taxTotal` -- `discount_total` -- `shipping_total` - -### `Lunar\Models\OrderLine` - -- `unit_price` -- `sub_total` -- `tax_total` -- `discount_total` -- `total` - -### `Lunar\Models\Transaction` - -- `amount` - -### `DefaultPriceFormatter` - -The default price formatter ships with Lunar and will handle most use cases for formatting a price, lets go through -them, first we'll create a standard price model. - -```php -$priceModel = \Lunar\Models\Price::create([ - // ... - 'price' => 1000, // Price is an int and should be in the lowest common denominator - 'min_quantity' => 1, -]); - -// Lunar\DataTypes\Price -$priceDataType = $priceModel->price; -``` - -Return the raw value, as it's stored in the database. - -```php -echo $priceDataType->value; // 1000 -``` - -Return the decimal representation for the price.The decimal value takes into account how many decimal places you have -set for the currency. So in this example if the -decimal places was 3 you would get 10.000 - -```php -echo $priceDataType->decimal(rounding: true); // 10.00 -echo $priceDataType->unitDecimal(rounding: true); // 10.00 -``` - -You may have noticed these two values are the same, so what's happening? Well the unit decimal will take into account -the unit quantity of the purchasable we have the price for. Let's show another example: - -```php -$productVariant = \Lunar\Models\ProductVariant::create([ - // ... - 'unit_quantity' => 10, -]); -``` - -By setting `unit_quantity` to 10 we're telling Lunar that 10 individual units make up this product at this price point, -this is useful if you're selling something that by itself would be under 1 cent i.e. 0.001EUR, which isn't a valid -price. - -```php -$priceModel = $productVariant->prices()->create([ - 'price' => 10, // 0.10 EUR -]); - -// Lunar\DataTypes\Price -$priceDataType = $priceModel->price; -``` - -Now lets try again: - -```php -echo $priceDataType->decimal(rounding: true); // 0.10 -echo $priceDataType->unitDecimal(rounding: true); // 0.01 -``` - -You can see the `unitDecimal` method has taken into account that `10` units make up the price so this gives a unit cost -of `0.01`. - -##### Formatting to a currency string - -The formatted price uses the native PHP [NumberFormatter](https://www.php.net/manual/en/class.numberformatter.php). If -you wish to specify a locale or formatting style you can, see the examples below. - -```php -$priceDataType->price->formatted('fr') // 10,00 £GB -$priceDataType->price->formatted('en-gb', \NumberFormatter::SPELLOUT) // ten point zero zero. -$priceDataType->price->formattedUnit('en-gb') // £10.00 -``` - -## Full reference for `DefaultPriceFormatter` - -```php -$priceDataType->decimal( - rounding: false -); - -$priceDataType->decimalUnit( - rounding: false -); - -$priceDataType->formatted( - locale: null, - formatterStyle: NumberFormatter::CURRENCY, - decimalPlaces: null, - trimTrailingZeros: true -); - -$priceDataType->unitFormatted( - locale: null, - formatterStyle: NumberFormatter::CURRENCY, - decimalPlaces: null, - trimTrailingZeros: true -); -``` - -## Creating a custom formatter - -Your formatter should implement the `PriceFormatterInterface` and have a constructor was accepts and sets -the `$value`, `$currency` and `$unitQty` properties. - -```php -currency) { - $this->currency = Currency::getDefault(); - } - } - - public function decimal(): float - { - // ... - } - - public function unitDecimal(): float - { - // ... - } - - public function formatted(): mixed - { - // ... - } - - public function unitFormatted(): mixed - { - // ... - } -} -``` - -The methods you implement can accept any number of arguments you want to support, you are not bound to what -the `DefaultPriceFormatter` accepts. - -Once you have implemented the required methods, simply swap it out in `config/lunar/pricing.php`: - -```php -return [ - // ... - 'formatter' => \App\Pricing\CustomPriceFormatter::class, -]; -``` - -## Model Casting - -If you have your own models which you want to use price formatting for, Lunar has a cast class you can use. The only -requirement is the column returns an `integer`. - -```php -class MyModel extends Model -{ - protected $casts = [ - //... - 'price' => \Lunar\Base\Casts\Price::class - ]; -} -``` \ No newline at end of file diff --git a/docs/core/reference/products.md b/docs/core/reference/products.md deleted file mode 100644 index 4fc39c7e58..0000000000 --- a/docs/core/reference/products.md +++ /dev/null @@ -1,832 +0,0 @@ -# Products - -## Overview - -Products are generally what you will be selling in your store. You define all your attributes against the product and -products can also have variations. In Lunar a product will always have at least one variation. From a UX point of view, -it will just look like you're editing a single product, but behind the scenes you'll be editing one variant. - -Products also belong to a `ProductType` and aside from the attributes, which you are free to define yourself, will have -a base SKU and a brand name. - -## Creating a product - -```php -Lunar\Models\Product::create([ - 'product_type_id' => $productTypeId, - 'status' => 'published', - 'brand_id' => $brandId, - 'attribute_data' => [ - 'name' => new TranslatedText(collect([ - 'en' => new Text('FooBar'), - ])), - 'description' => new Text('This is a Foobar product.'), - ], -]); -``` - -## Customer Groups - -You can assign customer groups to a product, this allows you to either always have that product enabled for the customer -group, or you can state which dates they should be active for (as long as the customer group is enabled). - -### Attaching customer groups - -```php - -// Will schedule for this product to be enabled in 14 days for this customer group. -$product->scheduleCustomerGroup($customerGroup, now()->addDays(14)); - -// Schedule the product to be enabled straight away -$product->scheduleCustomerGroup($customerGroup); - -// The schedule method will accept an array or collection of customer groups. -$product->scheduleCustomerGroup(CustomerGroup::get()); -``` - -### Retrieving products for a customer group - -If you want to get all products related to a customer group, you can use the `customerGroup` scope. This scope can -either take a single customer group (or customer group id) or a collection/array of ids/customer group models. - -```php -// Can be an array of ids -$products = \Lunar\Models\Product::customerGroup(1)->paginate(50); - -$products = \Lunar\Models\Product::customerGroup([ - $groupA, - $groupB, -])->paginate(50); -``` - -## Product Types - -e.g. Television, T-Shirt, Book, Phone, etc. - -Assigns the appropriate attributes for the product type. - -### Creating a product type - -```php -Lunar\Models\ProductType::create([ - 'name' => 'Boots', -]); -``` - -Product Types also have [Attributes](/core/reference/attributes) associated to them. These associated attributes will -determine what fields are available to products when editing. For example if you had an attribute of `Screen Type` -associated to a `TVs` product type, any products with that product type would have access to that attribute when -editing. - -You can associate attributes to a product type like so (it's just a straight -forward [Polymorphic relationship](https://laravel.com/docs/8.x/eloquent-relationships#many-to-many-polymorphic-relations)). - -```php -$productType->mappedAttributes()->attach([ /* attribute ids ... */ ]); -``` - -You can associate both `Product` and `ProductVariant` attributes to a product type which will then display on either the -product or product variant when editing. - -::: warning -If you decide to delete an attribute, this will cause the association to be dropped and you could lose data. -::: - -### Retrieving the product type relationship - -If you have a product, you can fetch its product type like so: - -```php -$product->productType; - -$product->load(['productType']); -``` - -## Product Identifiers - -You can choose to add product identifiers to each product variant. These are fields which, as the name suggests, allow -you to identify a product and its variants for use in your internal systems. You can choose whether these are required -and unique in the hub whilst editing. - -### Available fields - -**SKU** - -SKU stands for “stock keeping unit” and — as the name suggests — it is a number (usually eight alphanumeric digits) that -you can assign to products to keep track of stock levels internally. If a product has different colors and sizes, each -variation may use a unique SKU number. - -**GTIN** - -A Global Trade Item Number (GTIN) is a unique and internationally recognized identifier for a product. These usually -accompany a barcode and are useful when using services such as Google to help them classify the product. - -**MPN** - -MPN (Manufacturer Part Number) is the product identifier used to differentiate a product among other (similar) products -from the same brand/manufacturer, making it easier for customers to find and buy your products and protecting them from -counterfeit. - -**EAN** - -European Article numbering code (EAN) is a series of letters and numbers in a unique order that helps identify specific -products within your own inventory. - -### Validation - -Depending on your storefront needs, you might not need any of these fields to be required or unique. For this reason you -can change this behaviour at a validation level. - -`config/lunar-hub/products.php` - -```php - 'sku' => [ - 'required' => true, - 'unique' => false, - ], - 'gtin' => [ - 'required' => false, - 'unique' => false, - ], - 'mpn' => [ - 'required' => false, - 'unique' => false, - ], - 'ean' => [ - 'required' => false, - 'unique' => false, - ], -``` - -## Product Options - -These are what you use to define the different options a product has available to it. These are directly related to the -different variants a product might have. Each `ProductOption` will have a set of `ProductOptionValue` models related to -it. For example: - -You could have a `ProductOption` called "Colour" and then multiple `ProductionOptionValue` models for each colour you -would like to offer. - -::: tip -Product options and Product option values are defined at a system level and are translatable. -::: - -### Creating a `ProductOption` - -```php -$option = Lunar\Models\ProductOption::create([ - 'name' => [ - 'en' => 'Colour', - 'fr' => 'Couleur', - ], - 'label' => [ - 'en' => 'Colour', - 'fr' => 'Couleur', - ], -]); -``` - -We can then create values for this option: - -```php -// Lunar\Models\ProductOptionValue -$option->values()->createMany([ - [ - 'name' => [ - 'en' => 'Blue', - 'fr' => 'Bleu', - ], - ], - [ - 'name' => [ - 'en' => 'Red', - 'fr' => 'Rouge', - ], - ], -]); -``` - -This Product option and it's values are now ready to be used to with Product Variants. - -# Product Shipping - -By default Lunar will mark all product variants as `shippable`. If you don't need to ship a certain variant then you can -simply set this to false. - -```php -$variant->update([ - 'shippable' => false, -]); -``` - -## Product dimensions - -Product's can have different dimensions assigned to them, the values we have available are - -- Length -- Width -- Height -- Weight -- Volume - -For handling conversions, we use the [Cartalyst Converter](https://github.com/cartalyst/converter/tree/master) package. -This supports a wide range of UOM and can convert those values to different measurements. - -Each UOM has a corresponding `_value` and `_unit` column in the database. For example: - -```php -length_value -length_unit -width_value -width_unit -``` - -### Configuring measurements - -You can configure the available UOM's in the `lunar/shipping.php` config file. Here is an example of what Lunar provides -by default: - -**Length** - -- m -- mm -- cm -- ft -- in - -**Weight** - -- kg -- g -- lbs - -**Volume** - -- l -- ml -- gal -- floz - -### Getting and converting measurement values - -You are free to access to the `*_value` and `*_unit` values for the variant and use them in their raw form, however we -do offer an accessor for each unit that you can use: - -```php -$variant->length->to('length.ft')->convert(); -``` - -#### Volume calculation - -Volume is calculated different to the rest of the measurements. You can either have it automatically figure out the -volume or manually set it yourself: - -```php -$variant->update([ - 'length_value' => 50, - 'length_unit' => 'mm', - 'height_value' => 50, - 'height_unit' => 'mm', - 'width_value' => 50, - 'width_unit' => 'mm', -]) - -// By default will return ml -$variant->volume->getValue(); // 125 - -// Use any UOM you want for volume -$variant->volume->to('volume.l')->convert()->getValue(); // 12.5 - -// Set the volume manually and it'll always use that. -$variant->update([ - 'volume_unit' => 'floz', - 'volume_value' => 100 -]); - -$variant->volume->getValue(); // 100 - -$variant->volume->to('volume.l')->convert()->getValue(); // 2.95735... -``` - -**Get a formatted value** - -If you want to display a formatted value, you can! - -```php -$variant->length->format(); // 50cm -``` - -## Variants - -Variants allow you to specify different variations of a product. Think things like Small Blue T-shirt, or Size 8 and -Size 9 Leather Boots. Your product is the main parent and your variants are based off that product to create multiple -permutations. - -Variants are also responsible for storing data such as Pricing, Inventory/Stock information, Shipping information etc. -For that reason a product will always have at least one variant. - -When you decide you want to offer more than one variant for a product, upon generation, Lunar will take the first -variant and use that as a base for all other variants in terms of pricing, inventory etc. So you won't lose any data you -may have already saved against the existing product. - -## Creating variants - -### New Product - -If you have a new product, you would create your variants in bulk based on an array of `ProductOptionValue` IDs. - -You will need to determine what variants you need to create and assign the correct option values to that variant. - -For example, lets say we have an option "Colour" and we want to create "Blue" and "Red" variants. - -::: tip -A product variant will require a product, currency and a tax class. -If you do not have these entities created, you will need to do so before continuing. -::: - -If needed, Create the product and tax class. - -```php -$product = Product::create([...]); -$taxClass = TaxClass::create([...]); -$currency = Currency::create([...]); -``` - -Otherwise, fetch them. - -```php -$product = Product::where(...)->first(); -$taxClass = TaxClass::where(...)->first(); -$currency = Currency::where(...)->first(); -``` - -You can also specify a status to limit results to using the provided scope. - -```php -Product::status('published')->get(); -``` - -Then we need to create our base option and it's values. - -```php -$option = \Lunar\Models\ProductOption::create([ - 'name' => [ - 'en' => 'Colour', - ]; - 'label' => [ - 'en' => 'Colour', - ]; -]); - -$blueOption = $option->values()->create([ - 'name' => [ - 'en' => 'Blue', - ], -]); - -$redOption = $option->values()->create([ - 'name' => [ - 'en' => 'Red', - ], -]); -``` - -From here we create our variant, associate it with a product and a tax class, then attach the option id. - -```php -$blueVariant = ProductVariant::create([ - 'sku' => 'blue-product', -]); - -$blueVariant->values()->attach($blueOption); - -$redVariant = ProductVariant::create([ - 'product_id' => $product->id, - 'tax_class_id' => $taxClass->id, - 'sku' => 'red-product', -]); - -$redVariant->values()->attach($redOption); -``` - -Now, we need to create a price for our variant. This is where we use our *currency* created or fetched earlier. - -```php -$variant->prices()->create([ - 'price' => 199, - 'currency_id' => $currency->id, -]); -``` - -### Exceptions - -When creating variants there are some exceptions that will be thrown if certain conditions are met. - -| Exception | Conditions | -|:-------------------------------------------------|:---------------------------------------------------------------------------------------| -| `Illuminate\Validation\ValidationException` | Thrown if validation fails on the value options array. | - -## Pricing - -### Overview - -Prices are stored in the database as integers. When retrieving a `Price` model the `price` and `compare_price` -attributes are cast to a `Price` datatype. This casting gives you some useful helpers when dealing with prices on your -front end. - -| Field | Description | Default | Required | -|:--------------------|:-------------------------------------------------------------------------------------|:--------|:---------| -| `price` | A integer value for the price | `null` | yes | -| `compare_price` | For display purposes, allows you to show a comparison price, e.g. RRP. | `null` | no | -| `currency_id` | The ID of the related currency | `null` | yes | -| `min_quantity` | The minimum quantity required to get this price. | `1` | no | -| `customer_group_id` | The customer group this price relates to, leaving as `null` means any customer group | `null` | no | -| `priceable_type` | This is the class reference to the related model which owns the price | `null` | yes | -| `priceable_id` | This is the id of the related model which owns the price | `null` | yes | - -```php -$priceable = \Lunar\Models\ProductVariant::create([/** ... */]); -$price = \Lunar\Models\Price::create([ - 'price' => 199, - 'compare_price' => 299, - 'currency_id' => 1, - 'min_quantity' => 1, - 'customer_group_id' => null, - 'priceable_type' => $priceable->getMorphClass(), - 'priceable_id' => $priceable->id, -]); -``` - -## Price formatting - -For the full reference on how to format prices for your storefront see the [Pricing Reference](/core/reference/pricing) - -::: tip -The same methods apply to the compare_price attribute -::: - -### Base Pricing - -Pricing is defined on a variant level, meaning you will have a different price for each variant and also for each -currency in the system. In order to add pricing to a variant, you can either create the model directly or use the -relationship method. - -```php -$priceable = \Lunar\Models\ProductVariant::create([/** ... */]); -\Lunar\Models\Price::create([ - 'price' => 199, - 'compare_price' => 299, - 'currency_id' => 1, - 'min_quantity' => 1, - 'customer_group_id' => null, - 'priceable_type' => $priceable->getMorphClass(), - 'priceable_id' => $priceable->id, -]); -``` - -```php -$variant->prices()->create([/* .. */]); -``` - -### Customer group pricing - -You can specify which customer group the price applies to by setting the `customer_group_id` column. If left as `null` -the price will apply to all customer groups. This is useful if you want to have different pricing for certain customer -groups and also different price quantity breaks per customer group. - -### Price Break Pricing - -Price Break pricing is a concept in which when you buy in bulk, the cost per item will change (usually go down). With Pricing -on Lunar, this is determined by the `min_quantity` column when creating prices. For example: - -```php -Price::create([ - // ... - 'price' => 199, - 'compare_price' => 399, - 'min_quantity' => 1, -]); - -Price::create([ - // ... - 'price' => 150, - 'compare_price' => 399, - 'min_quantity' => 10, -]); -``` - -In the above example if you order between 1 and 9 items you will pay `1.99` per item. But if you order at least 10 you -will pay `1.50` per item. - -## Fetching the price - -Once you've got your pricing all set up, you're likely going to want to display it on your storefront. We've created -a `PricingManager` which is available via a facade to make this process as painless as possible. - -To get the pricing for a product you can simply use the following helpers: - -#### Minimum example - -A quantity of 1 is implied when not passed. - -```php -$pricing = \Lunar\Facades\Pricing::for($variant)->get(); -``` - -#### With Quantities - -```php -$pricing = \Lunar\Facades\Pricing::qty(5)->for($variant)->get(); -``` - -#### With Customer Groups - -If you don't pass in a customer group, Lunar will use the default, including any pricing that isn't specific to a -customer group. - -```php -$pricing = \Lunar\Facades\Pricing::customerGroups($groups)->for($variant)->get(); - -// Or a single customer group -$pricing = \Lunar\Facades\Pricing::customerGroup($group)->for($variant)->get(); -``` - -#### Specific to a user - -The PricingManager assumes you want the price for the current authenticated user. - -If you want to always return the guest price, you may use... - -```php -$pricing = \Lunar\Facades\Pricing::guest()->for($variant)->get(); -``` - -Or to specify a different user... - -```php -$pricing = \Lunar\Facades\Pricing::user($user)->for($variant)->get(); -``` - -#### With a specific currency - -If you don't pass in a currency, the default is implied. - -```php -$pricing = \Lunar\Facades\Pricing::currency($currency)->for($variant)->get(); -``` - -#### For a model - -Assuming you have a model that implements the `hasPrices` trait, such as a `ProductVariant`, you can use the following -to retrieve pricing. - -```php -$pricing = $variant->pricing()->qty(5)->get(); -``` - -::: danger Be aware -If you try and fetch a price for a currency that doesn't exist, a ` Lunar\Exceptions\MissingCurrencyPriceException` -exception will be thrown. -::: - ---- - -This will return a `PricingResponse` object which you can interact with to display the correct prices. Unless it's a -collection, each property will return a `Lunar\Models\Price` object. - -```php -/** - * The price that was matched given the criteria. - */ -$pricing->matched; - -/** - * The base price associated to the variant. - */ -$pricing->base; - -/** - * A collection of all the price quantity breaks available for the given criteria. - */ -$pricing->priceBreaks; - -/** - * All customer group pricing available for the given criteria. - */ -$pricing->customerGroupPrices; -``` - -Sometimes you might want to simply get all prices a product has from the variants, instead of loading up all a products -variants fetching the prices that way, you can use the `prices` relationship on the product. - -```php -$product->prices -``` - -This will return a collection of `Price` models. - -### Storing Prices Inclusive of Tax - -Lunar allows you to store pricing inclusive of tax if you need to. This is helpful if you need to show charm pricing, at -$9.99 for example, which may not be possible if pricing is stored exclusive of tax due to rounding. - -To start you will need to set the `stored_inclusive_of_tax` config value in `lunar/pricing` to `true`. Then you will -need to ensure your default Tax Zone is set up correctly with the correct tax rates. - -Once set, the cart will automatically calculate the tax for you. - -If you need to show both ex. and inc. tax pricing on your product pages, you can use the following methods which are -available on the `Lunar\Models\Price` model. - -```php -$price->priceIncTax(); -$price->priceExTax(); -$price->comparePriceIncTax(); // to retrieve the tax inclusive compare price -``` - -### Customising Prices with Pipelines - -All pipelines are defined in `config/lunar/pricing.php` - -```php -'pipelines' => [ - //, -], -``` - -You can add your own pipelines to the configuration, they might look something like: - -```php -pricing->matched; - - $matchedPrice->price->value = 200; - - $pricingManager->pricing->matched = $matchedPrice; - - return $next($pricingManager); - } -} -``` - -```php -'pipelines' => [ - // ... - App\Pipelines\Pricing\CustomPricingPipeline::class, -], -``` - -::: tip -Pipelines will run from top to bottom -::: - -## Full Example - -For this example, we're going to be creating some Dr. Martens boots. Below is a screenshot of what we're aiming for: - - - -Here are the steps we're going to take: - -- Create our product type -- Create the initial product -- Create the product options and their values -- Generate the variants based on those values - -### Set up the product type. - -```php -$productType = Lunar\Models\ProductType::create([ - 'name' => 'Boots', -]); -``` - -::: tip Note -This example assumes we already have Attributes set up for name and description and that they're assigned to the product -type. -::: - -### Create the initial product - -```php -Lunar\Models\Product::create([ - 'product_type_id' => $productType->id, - 'status' => 'published', - 'brand_id' => $brandId, - 'sku' => 'DRBOOT', - 'attribute_data' => [ - 'name' => new TranslatedText(collect([ - 'en' => new Text('1460 PATENT LEATHER BOOTS'), - ])), - 'description' => new Text('Even more shades from our archive...'), - ], -]); -``` - -### Product Options - -Based on the example above we're going to need 2 options, Size and Colour. - -```php -$colour = Lunar\Models\ProductOption::create([ - 'name' => [ - 'en' => 'Colour', - ], - 'label' => [ - 'en' => 'Colour', - ], -]); - -$size = Lunar\Models\ProductOption::create([ - 'name' => [ - 'en' => 'Size', - ], - 'label' => [ - 'en' => 'Size', - ], -]); -``` - -### Product Option Values - -From here we now need to create our option values like so: - -```php -$colour->values()->createMany([ - [ - 'name' => [ - 'en' => 'Black', - ], - ], - [ - 'name' => [ - 'en' => 'White', - ], - ], - [ - 'name' => [ - 'en' => 'Pale Pink', - ], - ], - [ - 'name' => [ - 'en' => 'Mid Blue', - ], - ], -]); - -// We won't do all the sizes here, just enough to get the idea... -$size->values()->createMany([ - [ - 'name' => [ - 'en' => '3', - ], - ], - [ - 'name' => [ - 'en' => '6', - ], - ], -]); -``` - -### Generate the variants - -First we just need to grab the values we want to use to generate the variants. Since we're generating them for -everything, we just grab all of them. - -```php -$optionValueIds = $size->values->merge($colour->values)->pluck('id'); - -\Lunar\Hub\Jobs\Products\GenerateVariants::dispatch($product, $optionValueIds); -``` - -::: tip -When generating variants, the sku will be derived from the Product's base SKU, in this case `DRBOOT` and will be -suffixed with `-{count}`. -::: - -The resulting generation is as follows: - -| SKU | Colour | Size | -|:---------|:----------|:-----| -| DRBOOT-1 | Black | 3 | -| DRBOOT-2 | Black | 6 | -| DRBOOT-3 | White | 3 | -| DRBOOT-4 | White | 6 | -| DRBOOT-5 | Pale Pink | 3 | -| DRBOOT-6 | Pale Pink | 6 | -| DRBOOT-7 | Mid Blue | 3 | -| DRBOOT-8 | Mid Blue | 6 | - -You are then free to change the SKU's as you see fit, update the pricing for each variant etc before publishing. diff --git a/docs/core/reference/search.md b/docs/core/reference/search.md deleted file mode 100644 index 705ccf870e..0000000000 --- a/docs/core/reference/search.md +++ /dev/null @@ -1,63 +0,0 @@ -# Search - -## Overview - -Search is configured using the [Laravel Scout](https://laravel.com/docs/scout) package. - -Using Scout allows us to provide search out the box but also make it easy for you as the developer to customise and tailor searching to your needs. - -## Initial set up - -The database driver provides basic search to get you up and running but you will likely find you want to implement something with a bit more power, such as [Meilisearch](https://www.meilisearch.com/) or [Algolia](https://www.algolia.com/). - -## Configuration - -By default, scout has the setting `soft_delete` set to `false`. You need to make sure this is set to `true` otherwise you will see soft deleted models appear in your search results. - -If you want to use other models or your own models in the search engine, you can add the reference for them on the config file. - -```php -'models' => [ - // These models are required by the system, do not change them. - \Lunar\Models\Collection::class, - \Lunar\Models\Product::class, - \Lunar\Models\ProductOption::class, - \Lunar\Models\Order::class, - \Lunar\Models\Customer::class, - // Below you can add your own models for indexing - ] -``` - -## Index records - -If you installed the Lunar package in an existing project and you would like to use the database records with the search engine, or you just need to do some maintenance on the indexes, you can use the index command. - -```sh -php artisan lunar:search:index -``` - -The command will import the records of the models listed in the `lunar/search.php` configuration file. Type `--help` to see the available options. - -## Meilisearch - -If you used the Meilisearch package you would like to use the command to create filterable and searchable attributes on Meilisearch indexes. - -```sh -php artisan lunar:meilisearch:setup -``` - -## Engine Mapping - -By default, Scout will use the driver defined in your .env file as `SCOUT_DRIVER`. So if that's set to `meilisearch`, all your models will be indexed via the Meilisearch driver. This can present some issues, if you wanted to use a service like Algolia for Products, you wouldn't want all your Orders being indexed there since it will ramp up the record count and the cost. - -In Lunar we've made it possible to define what driver you would like to use per model. It's all defined in the `config/lunar/search.php` config file and looks like this: - -```php -'engine_map' => [ - \Lunar\Models\Product::class => 'algolia', - \Lunar\Models\Order::class => 'meilisearch', - \Lunar\Models\Collection::class => 'meilisearch', -], -``` - -It's quite self explanatory, if a model class isn't added to the config, it will take on the Scout default. diff --git a/docs/core/reference/tags.md b/docs/core/reference/tags.md deleted file mode 100644 index bcdc1dd22c..0000000000 --- a/docs/core/reference/tags.md +++ /dev/null @@ -1,48 +0,0 @@ -# Tags - -## Overview - -Tags serve a simple function in Lunar, you can add tags to models. This is useful for relating otherwise unrelated models in the system. They will also impact other parts of the system such as Dynamic collections. - -For example, you could have two products "Blue T-Shirt" and "Blue Shoes", which in their nature are unrelated, but you could add a `BLUE` tag to each product and then create a Dynamic Collection to include any products with a `BLUE` tag and they will be returned. - -::: tip -Heads up! Tags are converted uppercase as they are saved. -::: - -## Enabling tags - -In order to enable tagging on a model, simply add the `HasTags` trait. - -```php -syncTags($tags); - -$model->tags; - -// ['TAG ONE', 'TAG TWO', 'TAG THREE'] -``` - -If a tag exists already by name it will use it, otherwise they will be created. The process runs via a job so will run in the background if you have that set up. diff --git a/docs/core/reference/taxation.md b/docs/core/reference/taxation.md deleted file mode 100644 index e93596149c..0000000000 --- a/docs/core/reference/taxation.md +++ /dev/null @@ -1,161 +0,0 @@ -# Taxation - -## Overview - -No one likes taxes! But we have to deal with them... Lunar provides manual tax rules to implement the correct sales tax for each order. For complex taxation (e.g. US States) we suggest integrating with a service such as [TaxJar](https://www.taxjar.com/). - - -## Tax Classes - -Tax Classes are assigned to Products and allow us to classify products to certain taxable groups that may have differing tax rates. - -```php -Lunar\Models\TaxClass -``` - -|Field|Description| -|:-|:-| -|id|| -|name|e.g. `Clothing`| -|created_at|| -|updated_at|| - -```php -$taxClass = TaxClass::create([ - 'name' => 'Clothing', -]); -``` - -## Tax Zones - -These specify a geographic zone for tax rates to be applied. Tax Zones can be based upon countries, states or zip/post codes. - -```php -Lunar\Models\TaxZone -``` - -|Field| Description | -|:-|:------------------------------------| -|id| | -|name| e.g. `UK` | -|zone_type| `country`, `states`, or `postcodes` | -|price_display| `tax_inclusive` or `tax_exclusive` | -|active| true/false | -|default| true/false | -|created_at| | -|updated_at| | - -```php -$taxZone = TaxZone::create([ - 'name' => 'UK', - 'zone_type' => 'country', - 'price_display' => 'tax_inclusive', - 'active' => true, - 'default' => true, -]); -``` - -```php -Lunar\Models\TaxZoneCountry -``` - -|Field|Description| -|:-|:-| -|id|| -|tax_zone_id|| -|country_id|| -|created_at|| -|updated_at|| - - -```php -$taxZone->countries()->create([ - 'country_id' => \Lunar\Models\Country::first()->id, -]); -``` - -```php -Lunar\Models\TaxZoneState -``` - -|Field|Description| -|:-|:-| -|id|| -|tax_zone_id|| -|state_id|| -|created_at|| -|updated_at|| - -```php -$taxZone->states()->create([ - 'state_id' => \Lunar\Models\State::first()->id, -]); -``` - -```php -Lunar\Models\TaxZonePostcode -``` - -|Field|Description| -|:-|:-| -|id|| -|tax_zone_id|| -|country_id|| -|postcode|wildcard, e.g. `9021*`| -|created_at|| -|updated_at|| - -```php -Lunar\Models\TaxZoneCustomerGroup -``` - -|Field|Description| -|:-|:-| -|id|| -|tax_zone_id|| -|customer_group_id|| -|created_at|| -|updated_at|| - - -## Tax Rates - -Tax Zones have one or many tax rates. E.g. you might have a tax rate for the State and also the City, which would collectively make up the total tax amount. - -```php -Lunar\Models\TaxRate -``` - -|Field|Description| -|:-|:-| -|id|| -|tax_zone_id|| -|name|e.g. `UK`| -|created_at|| -|updated_at|| - -```php -Lunar\Models\TaxRateAmount -``` - -|Field|Description| -|:-|:-| -|id|| -|tax_rate_id|| -|tax_class_id|| -|percentage|e.g. `6` for 6%| -|created_at|| -|updated_at|| - - -## Settings -- Shipping and other specific costs are assigned to tax classes in the settings. -- Calculate tax based upon Shipping or Billing address? -- Default Tax Zone - -## Extending - -Sometimes the standard tax calculations aren't enough, and you may want to implement your own logic, perhaps connecting to a Tax service such as TaxJar. - -Lunar allows you to implement your own tax driver, check the [Extending Lunar](/core/extending/taxation) section for more information. - diff --git a/docs/core/reference/urls.md b/docs/core/reference/urls.md deleted file mode 100644 index dd51447382..0000000000 --- a/docs/core/reference/urls.md +++ /dev/null @@ -1,157 +0,0 @@ -# URLs - -## Overview - -URLs are not to be confused with Routes in Laravel. You can create routes for a number of resources, but most commonly they would be created for a Product. - -They allow you to add a way to identify/query for a resource without having to use the ID of that resource. Notably these would be useful for vanity URLs so instead of something like: - -```bash -/products/1 -``` - -On your storefront, you could have: - -```bash -/products/apple-iphone -``` - -`apple-iphone` is the slug for a URL which would correspond to a product and would allow you to fetch it easily without having to expose IDs or do any weird round trips to your API. - -::: tip -A URL cannot share the same `slug` and `language_id` columns. You can also only have one `default` URL per language for that resource. -::: - -## Creating a URL - -```php -\Lunar\Models\Url::create([ - 'slug' => 'apple-iphone', - 'language_id' => $language->id, - 'default' => true, -]); -``` - -::: tip -If you add a new default URL for a language which already has one, the new URL will override and become the new default. -::: - -```php -$urlA = \Lunar\Models\Url::create([ - 'slug' => 'apple-iphone', - 'language_id' => 1, - 'default' => true, -]); - -$urlA->default // true - -$urlB = \Lunar\Models\Url::create([ - 'slug' => 'apple-iphone-26', - 'language_id' => 1, - 'default' => true, -]); - -$urlA->default // false -$urlB->default // true - -/** - * Since this is a different language, no other URLs will be changed. - **/ -$urlC = \Lunar\Models\Url::create([ - 'slug' => 'apple-iphone-french', - 'language_id' => 2, - 'default' => true, -]); - - -$urlA->default // false -$urlB->default // true -$urlC->default // true -``` - -## Deleting a URL - -When you delete a URL, if it was the default then Lunar will look for a non default of the same language and assign that instead. - - -```php -$urlA = \Lunar\Models\Url::create([ - 'slug' => 'apple-iphone', - 'language_id' => 1, - 'default' => true, -]); - -$urlB = \Lunar\Models\Url::create([ - 'slug' => 'apple-iphone-26', - 'language_id' => 1, - 'default' => false, -]); - -$urlB->default // false - -$urlA->delete(); - -$urlB->default // true -``` - - -## Adding URL support to Models - -Out the box Lunar has a few pre-configured models which have URLs - -- Products -- Collections - -You are free to add URLs to your own models. - -```php -urls; // Collection -``` - -## Automatically generating URLs - -You can tell Lunar to generate URLs for models that use the `HasUrls` trait automatically by setting the `generator` config option in `config/lunar/urls.php`. - -By default this is set to `Lunar\Generators\UrlGenerator::class` which means URLs will be generated. To disable this, set the config like below: - -```php -return [ - 'generator' => null -]; -``` - -By default this will use the default language and take the `name` attribute as the slug, you are of course free to use your own class for this. You just need to make sure there is a `handle` method which accepts a `Model`. - -```php -= 8.2 -- MySQL 8.0+ / PostgreSQL 9.2+ -- exif PHP extension (on most systems it will be installed by default) -- intl PHP extension (on most systems it will be installed by default) -- bcmath PHP extension (on most systems it will be installed by default) -- GD PHP extension (used for image manipulation) - -## Installation - -::: tip -We assume you have a suitable local development environment in which to run Lunar. We would highly suggest [Laravel Herd](https://herd.laravel.com) for this purpose. -::: - -### Create a New Project - -```bash -composer create-project --stability dev lunarphp/livewire-starter-kit my-store - -cd my-store -``` - -or using the latest Laravel Installer you can do... - -```bash -laravel new my-store --using=lunarphp/livewire-starter-kit - -cd my-store -``` - - -### Configure the Laravel app - -Copy the `.env.example` file to `.env` and make sure the details match to your install. - -```bash -cp .env.example .env -``` - -All the relevant configuration files should be present in the repo. - -### Migrate and Seed - -Install Lunar - -```bash -php artisan lunar:install -``` - -Seed the demo data. - -```bash -php artisan db:seed -``` - -Link the storage directory - -```bash -php artisan storage:link -``` - -## Finished 🚀 - -You are now installed! - -- You can access the storefront at `http://This addon enables Opayo payments on your Lunar storefront.
- -## Alpha Release - -This addon is currently in Alpha, whilst every step is taken to ensure this is working as intended, it will not be considered out of Alpha until more tests have been added and proved. - -## Minimum Requirements - -- Lunar `1.x` -- An [Elavon](https://www.elavon.com/) merchant account - -## Installation - -### Require the composer package - -```sh -composer require lunarphp/opayo -``` - -### Configure the service - -Add the opayo config to the `config/services.php` file. - -```php -// ... -'opayo' => [ - 'vendor' => env('OPAYO_VENDOR'), - 'env' => env('OPAYO_ENV', 'test'), - 'key' => env('OPAYO_KEY'), - 'password' => env('OPAYO_PASSWORD'), - 'host' => env('OPAYO_HOST'), -], -``` - - -### Enable the driver - -Set the driver in `config/lunar/payments.php` - -```php - [ - 'card' => [ - // ... - 'driver' => 'opayo', - ], - ], -]; -``` - - - -## Configuration - -Below is a list of the available configuration options this package uses in `config/lunar/opayo.php` - -| Key | Default | Description | -| --- | --- | --- | -| `policy` | `automatic` | Determines the policy for taking payments and whether you wish to capture the payment manually later or take payment straight away. Available options `deferred` or `automatic` | - ---- - -## Backend Usage - -### Get a merchant key - -```php -Lunar\Opayo\Facades\Opayo::getMerchantKey(); -``` - -### Authorize a charge - -```php -$response = \Lunar\Facades\Payments::driver('opayo')->cart( - $cart = CartSession::current()->calculate() -)->withData([ - 'merchant_key' => $request->get('merchantSessionKey'), - 'card_identifier' => $request->get('cardToken'), - 'browserLanguage' => $request->get('browserLanguage'), - 'challengeWindowSize' => $request->get('challengeWindowSize'), - 'browserIP' => $request->ip(), - 'browserAcceptHeader' => $request->header('accept'), - 'browserUserAgent' => $request->get('browserUserAgent'), - 'browserJavaEnabled' => $request->get('browserJavaEnabled', false), - 'browserColorDepth' => $request->get('browserColorDepth'), - 'browserScreenHeight' => $request->get('browserScreenHeight'), - 'browserScreenWidth' => $request->get('browserScreenWidth'), - 'browserTZ' => $request->get('browserTZ'), - 'status' => 'payment-received', -])->authorize(); -``` - -When authorizing a charge, you may be required to submit extra authentication in the form of 3DSV2, you can handle this in your payment endpoint. - -```php -if (is_a($response, \Lunar\Opayo\Responses\ThreeDSecureResponse::class)) { - return response()->json([ - 'requires_auth' => true, - 'data' => $response, - ]); -} -``` - -`$response` will contain all the 3DSV2 information from Opayo. - -You can find more information about this using the following links: - -- [3-D Secure explained](https://www.elavon.co.uk/resource-center/help-with-your-solutions/opayo/fraud-prevention/3D-Secure.html) -- [3D Secure Transactions](https://developer.elavon.com/products/opayo-direct/v1/3d-secure-transactions) -- Stack overflow [SagePay 3D Secure V2 Flow](https://stackoverflow.com/questions/65329436/sagepay-3d-secure-v2-flow) - -Once you have handled the 3DSV2 response on your storefront, you can then authorize again. - -```php -$response = Payments::driver('opayo')->cart( - $cart = CartSession::current()->calculate() -)->withData([ - 'cres' => $request->get('cres'), - 'pares' => $request->get('pares'), - 'transaction_id' => $request->get('transaction_id'), -])->threedsecure(); - -if (! $response->success) { - abort(401); -} - -``` - -### Opayo card tokens - -When authenticated users make an order on your store, it can be good to offer the ability to save their card information for future use. Whilst we don't store the actual card details, we can use card tokens which represent the card the user has used before. - -> You must have saved payments enabled on your Opayo account because you can use these. - -To save a card, pass in the `saveCard` data key when authorizing a payment. - -```php -$response = \Lunar\Facades\Payments::driver('opayo')->cart( - $cart = CartSession::current()->calculate() -)->withData([ - // ... - 'saveCard' => true -])->authorize(); -``` - -Assuming everything went well, there will be a new entry in the `opayo_tokens` table, associated to the authenticated user. You can then display these card representations at checkout for the user to select. The `token` is what replaces the `card_identifier` data key. - -```php -$response = \Lunar\Facades\Payments::driver('opayo')->cart( - $cart = CartSession::current()->calculate() -)->withData([ - // ... - 'card_identifier' => $request->get('cardToken'), - 'reusable' => true -])->authorize(); -``` - -Responses are then handled the same as any other transaction. - -## Contributing - -Contributions are welcome, if you are thinking of adding a feature, please submit an issue first so we can determine whether it should be included. diff --git a/packages/paypal/README.md b/packages/paypal/README.md deleted file mode 100644 index 91c3a064cf..0000000000 --- a/packages/paypal/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Lunar PayPal Payments - -WIP diff --git a/packages/search/README.md b/packages/search/README.md deleted file mode 100644 index 7d6d9cd819..0000000000 --- a/packages/search/README.md +++ /dev/null @@ -1,125 +0,0 @@ - -## Lunar Search - -This packages brings E-Commerce search to Lunar. ---- - -## Requirements -- Lunar >= 1.x - -## License - -Lunar is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). - -## Installation - -### Require the composer package - -```sh -composer require lunarphp/search -``` - -## Usage - -### Configuration - -Most configuration is done via `config/lunar/search.php`. Here you can specify which facets should be used and how they are displayed. - -```php -'facets' => [ - \Lunar\Models\Product::class => [ - 'brand' => [ - 'label' => 'Brand', - ], - 'colour' => [ - 'label' => 'Colour', - ], - 'size' => [ - 'label' => 'Size', - ], - 'shoe-size' => [ - 'label' => 'Shoe Size', - ] - ] -], -``` - -### Basic Search - -At a basic level, you can search models using the provided facade. - -```php -use Lunar\Search\Facades\Search; - -// Search on a specific model -$results = Search::on(\Lunar\Models\Collection::class)->query('Hoodies')->get(); - -// Search on Lunar\Models\Product by default. -$results = Search::query('Hoodies')->get(); -``` - -Under the hood this will detect what Scout driver is mapped under `lunar.search.engine_map` and -then perform a search using that given driver. To increase performance the results will not be -hydrated from the database, but instead will be the raw results from the search provider. - -### Response format - -This packages makes use of Spatie Data to transform search results from the provider into consistent responses you can use on your frontend. - -You can view all available Data classes in the `src/Data` directory. - -### Transforming Data classes to Typescript - -If you use Spatie Typescript Transformer, you can add the following to your `typescript-transformer.php` config: - -```php -return [ - //... - 'auto_discover_types' => [ - // ... - \Lunar\Search\data_path(), - ], -]; -``` -Then in your Javascript the data classes will be available under the `Lunar.Search` namespace. - -```js -defineProps<{ - results: Lunar.Search.SearchResults -}>() -``` - -### Handling the response - -Searching returns a `Lunar\Data\SearchResult` DTO which you can use in your templates: - -```php -use Lunar\Search\Facades\Search; -$results = Search::query('Hoodies')->get(); -``` - -```bladehtml - -@foreach($results->hits as $hit) - {{ $hit->document['name'] }} -@endforeach - - -@foreach($results->facets as $facet) - - {{ $facet->label }} - @foreach($facet->values as $facetValue) - - $facetValue->active, - ]) - > - {{ $facetValue->label }} - - {{ $facetValue->count }} - @endforeach -This addon enables Stripe payments on your Lunar storefront.
- -## Alpha Release - -This addon is currently in Alpha, whilst every step is taken to ensure this is working as intended, it will not be considered out of Alpha until more tests have been added and proved. - -## Tests required: - -- [ ] Successful charge response from Stripe. -- [ ] Unsuccessful charge response from Stripe. -- [ ] Test `manual` config reacts appropriately. -- [x] Test `automatic` config reacts appropriately. -- [ ] Ensure transactions are stored correctly in the database -- [x] Ensure that the payment intent is not duplicated when using the same Cart -- [ ] Ensure appropriate responses are returned based on Stripe's responses. -- [ ] Test refunds and partial refunds create the expected transactions -- [ ] Make sure we can manually release a payment or part payment and handle the different responses. - -## Minimum Requirements - -- Lunar `1.x` -- A [Stripe](http://stripe.com/) account with secret and public keys - -## Optional Requirements - -- Laravel Livewire (if using frontend components) -- Alpinejs (if using frontend components) -- Javascript framework - -## Installation - -### Require the composer package - -```sh -composer require lunarphp/stripe -``` - -### Publish the configuration - -This will publish the configuration under `config/lunar/stripe.php`. - -```sh -php artisan vendor:publish --tag=lunar.stripe.config -``` - -### Publish the views (optional) - -Lunar Stripe comes with some helper components for you to use on your checkout, if you intend to edit the views they provide, you can publish them. - -```sh -php artisan vendor:publish --tag=lunar.stripe.components -``` - -### Enable the driver - -Set the driver in `config/lunar/payments.php` - -```php - [ - 'card' => [ - // ... - 'driver' => 'stripe', - ], - ], -]; -``` - -### Add your Stripe credentials - -Make sure you have the Stripe credentials set in `config/services.php` - -```php -'stripe' => [ - 'key' => env('STRIPE_SECRET'), - 'public_key' => env('STRIPE_PK'), - 'webhooks' => [ - 'lunar' => env('LUNAR_STRIPE_WEBHOOK_SECRET'), - ], -], -``` - -> Keys can be found in your Stripe account https://dashboard.stripe.com/apikeys - -## Configuration - -Below is a list of the available configuration options this package uses in `config/lunar/stripe.php` - -| Key | Default | Description | -|------------------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `policy` | `automatic` | Determines the policy for taking payments and whether you wish to capture the payment manually later or take payment straight away. Available options `manual` or `automatic` | -| `sync_addresses` | `true` | When enabled, the Stripe addon will attempt to sync the billing and shipping addresses which have been stored against the payment intent on Stripe. | ---- - -## Backend Usage - -### Create a PaymentIntent - -```php -use \Lunar\Stripe\Facades\Stripe; - -Stripe::createIntent(\Lunar\Models\Cart $cart, $options = []); -``` - -This method will create a Stripe PaymentIntent from a Cart and add the resulting ID to the meta for retrieval later. If a PaymentIntent already exists for a cart this will fetch it from Stripe and return that instead to avoid duplicate PaymentIntents being created. - -You can pass any additional parameters you need, by default the following are sent: - -```php -[ - 'amount' => 1099, - 'currency' => 'GBP', - 'automatic_payment_methods' => ['enabled' => true], - 'capture_method' => config('lunar.stripe.policy', 'automatic'), - // If a shipping address exists on a cart - // $shipping = $cart->shippingAddress - 'shipping' => [ - 'name' => "{$shipping->first_name} {$shipping->last_name}", - 'phone' => $shipping->contact_phone, - 'address' => [ - 'city' => $shipping->city, - 'country' => $shipping->country->iso2, - 'line1' => $shipping->line_one, - 'line2' => $shipping->line_two, - 'postal_code' => $shipping->postcode, - 'state' => $shipping->state, - ], - ] -] -``` - -```php -$paymentIntentId = $cart->meta['payment_intent']; // The resulting ID from the method above. -``` -```php -$cart->meta->payment_intent; -``` - -### Fetch an existing PaymentIntent - -```php -use \Lunar\Stripe\Facades\Stripe; - -Stripe::fetchIntent($paymentIntentId); -``` - -### Syncing an existing intent - -If a payment intent has been created and there are changes to the cart, you will want to update the intent so it has the correct totals. - -```php -use \Lunar\Stripe\Facades\Stripe; - -Stripe::syncIntent(\Lunar\Models\Cart $cart); -``` - - -### Update an existing intent - -For when you want to update certain properties on the PaymentIntent, without needing to recalculate the cart. - -See https://docs.stripe.com/api/payment_intents/update - -```php -use \Lunar\Stripe\Facades\Stripe; - -Stripe::updateIntent(\Lunar\Models\Cart $cart, [ - 'shipping' => [/*..*/] -]); -``` - -### Cancel an existing intent - -If you need to cancel a PaymentIntent, you can do so. You will need to provide a valid reason, those of which can be found in the Stripe docs: https://docs.stripe.com/api/payment_intents/cancel. - -Lunar Stripe includes a PHP Enum to make this easier for you: - -```php -use Lunar\Stripe\Enums\CancellationReason; - -CancellationReason::ABANDONED; -CancellationReason::DUPLICATE; -CancellationReason::REQUESTED_BY_CUSTOMER; -CancellationReason::FRAUDULENT; -``` - -```php -use Lunar\Stripe\Facades\Stripe; -use Lunar\Stripe\Enums\CancellationReason; - -Stripe::cancelIntent(\Lunar\Models\Cart $cart, CancellationReason $reason); -``` - -### Update the address on Stripe - -So you don't have to manually specify all the shipping address fields you can use the helper function to do it for you. - -```php -use \Lunar\Stripe\Facades\Stripe; - -Stripe::updateShippingAddress(\Lunar\Models\Cart $cart); -``` - -## Charges - -### Retrieve a specific charge - -```php -use \Lunar\Stripe\Facades\Stripe; - -Stripe::getCharge(string $chargeId); -``` - -### Get all charges for a payment intent - -```php -use \Lunar\Stripe\Facades\Stripe; - -Stripe::getCharges(string $paymentIntentId); -``` - -## Webhooks - -The add-on provides an optional webhook you may add to Stripe. You can read the guide on how to do this on the Stripe website [https://stripe.com/docs/webhooks/quickstart](https://stripe.com/docs/webhooks/quickstart). - -The events you should listen to are `payment_intent.payment_failed`, `payment_intent.succeeded`. - -The path to the webhook will be `http:://yoursite.com/stripe/webhook`. - -You can customise the path for the webhook in `config/lunar/stripe.php`. - -You will also need to add the webhook signing secret to the `services.php` config file: - -```php - [ - // ... - 'webhooks' => [ - 'lunar' => '...' - ], - ], -]; -``` - -If you do not wish to use the webhook, or would like to manually process an order as well, you are able to do so. - -```php -$cart = CartSession::current(); - -// With a draft order... -$draftOrder = $cart->createOrder(); -Payments::driver('stripe')->order($draftOrder)->withData([ - 'payment_intent' => $draftOrder->meta['payment_intent'], -])->authorize(); - -// Using just the cart... -Payments::driver('stripe')->cart($cart)->withData([ - 'payment_intent' => $cart->meta['payment_intent'], -])->authorize(); -``` - - -## Storefront Examples - -First we need to set up the backend API call to fetch or create the intent, this isn't Vue specific but will likely be different if you're using Livewire. - -```php -use \Lunar\Stripe\Facades\Stripe; - -Route::post('api/payment-intent', function () { - $cart = CartSession::current(); - - $cartData = CartData::from($cart); - - if ($paymentIntent = $cartData->meta['payment_intent'] ?? false) { - $intent = StripeFacade::fetchIntent($paymentIntent); - } else { - $intent = StripeFacade::createIntent($cart); - } - - if ($intent->amount != $cart->total->value) { - StripeFacade::syncIntent($cart); - } - - return $intent; -})->middleware('web'); -``` - -### Vuejs - -This is just using Stripe's payment elements, for more information [check out the Stripe guides](https://stripe.com/docs/payments/elements) - -### Payment component - -```js - -``` - -```html - - - -``` ---- - -## Extending - -### Webhook event params - -In order to process the payment intent and link it to an order, we need the PaymentIntent ID and an optional Order ID. - -By default Lunar Stripe will look for the PaymentIntent ID via the Stripe Event and try and determine whether an existing order ID has been defined on the PaymentIntent meta. - -You can customise this behaviour by overriding the `ProcessesEventParameters` instance. - -```php -// AppServiceProvider -use Lunar\Stripe\Concerns\ProcessesEventParameters; -use Lunar\Stripe\DataTransferObjects\EventParameters; - -public function boot() -{ - $this->app->instance(ProcessesEventParameters::class, new class implements ProcessesEventParameters - { - public function handle(\Stripe\Event $event): EventParameters - { - $paymentIntentId = $event->data->object->id; - // Setting $orderId to null will mean a new order is created. - $orderId = null; - - return new EventParameters($paymentIntentId, $orderId); - } - }); - -## Events - -Below are the events which are dispatched under specific situations within the addon. - -### `Lunar\Stripe\Events\Webhook\CartMissingForIntent` - -Dispatched when attempting to process a payment intent, but no matching Order or Cart model can be found. - -```php -public function handle(\Lunar\Stripe\Events\Webhook\CartMissingForIntent $event) -{ - echo $event->paymentIntentId; -} -``` - -## Contributing - -Contributions are welcome, if you are thinking of adding a feature, please submit an issue first so we can determine whether it should be included. - - -## Testing - -A [MockClient](https://github.com/lunar/stripe/blob/main/tests/Stripe/MockClient.php) class is used to mock responses the Stripe API will return. diff --git a/packages/table-rate-shipping/README.md b/packages/table-rate-shipping/README.md deleted file mode 100644 index 4b90525994..0000000000 --- a/packages/table-rate-shipping/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# Lunar Table Rate Shipping - - -# Requirements - -- LunarPHP Admin `>` `1.x` - -# Installation - -Install via Composer - -``` -composer require lunarphp/table-rate-shipping -``` - -Then register the plugin in your service provider - -```php -use Lunar\Admin\Support\Facades\LunarPanel; -use Lunar\Shipping\ShippingPlugin; -// ... - -public function register(): void -{ - LunarPanel::panel(function (Panel $panel) { - return $panel->plugin(new ShippingPlugin()); - })->register(); - - // ... -} -``` -# Getting Started -This addon provides an easy way for you to add different shipping to your storefront and allow your customers to choose from the different shipping rates you have set up, based on various factors such as zones, minimum spend etc - -## Shipping Methods - -Shipping Methods are the different ways in which your storefront can send orders to customers, you could also allow customers to collect their order from your store which this addon supports. - -## Shipping Zones - -Shipping Zones allow you to section of area's of the countries you ship to, providing you with an easy way to offer distinct shipping methods and pricing to each zone, each zone can be restricted by the following: - -- Postal codes -- Country -- State/Province (based on country) - -## Shipping Rates - -Shipping Rates are the prices you offer for each of your shipping zones, they are linked to a shipping method. So for example you might have a Courier Area Shipping Zone and an Everywhere Else Shipping Zone, you can offer different pricing restrictions using the same shipping methods. - -## Shipping Exclusion Lists - -Sometimes, you might not want to ship certain items to particular Shipping Zone, this is where exclusion lists come in. You can associate purchasables to a list which you can then associate to a shipping zone, if a cart contains any of them then they won't be able to select a shipping rate. - -# Storefront usage - -This addon uses the shipping modifiers provided by the Lunar core, so you shouldn't need to change your existing implementation. - -```php -$options = \Lunar\Base\ShippingManifest::getOptions( - $cart -); -``` - -# Advanced usage - -## Return available drivers - -```php -\Lunar\Shipping\Facades\Shipping::getSupportedDrivers(); -``` - -## Using the driver directly - -```php -\Lunar\Shipping\Facades\Shipping::with('ship-by')->resolve( - new \Lunar\Shipping\DataTransferObjects\ShippingOptionRequest( - shippingRate: \Lunar\Shipping\Models\ShippingRate $shippingRate, - cart: \Lunar\Models\Cart $cart - ) -); -``` - -## Shipping Zones - -Each method is optional, the more you add the more strict it becomes. - -```php -$shippingZones = Lunar\Shipping\Facades\Shipping::zones() - ->country(\Lunar\Models\Country $country) - ->state(\Lunar\Models\State $state) - ->postcode( - new \Lunar\Shipping\DataTransferObjects\PostcodeLookup( - country: \Lunar\Models\Country $country, - postcode: 'NW1' - ) - )->get() - -$shippingZones->map(/* .. */); -``` - -## Shipping Rates - -```php -$shippingRates = \Lunar\Shipping\Facades\Shipping::shippingRates( - \Lunar\Models\Cart $cart -); -``` - -## Shipping Options - -```php -$shippingOptions = \Lunar\Shipping\Facades\Shipping::shippingOptions( - \Lunar\Models\Cart $cart -); -``` \ No newline at end of file diff --git a/packages/table-rate-shipping/database/migrations/2024_03_19_100000_remap_shipping_polymorphic_relations.php b/packages/table-rate-shipping/database/migrations/2024_03_19_100000_remap_shipping_polymorphic_relations.php index 2bcae006b4..6716355ade 100644 --- a/packages/table-rate-shipping/database/migrations/2024_03_19_100000_remap_shipping_polymorphic_relations.php +++ b/packages/table-rate-shipping/database/migrations/2024_03_19_100000_remap_shipping_polymorphic_relations.php @@ -1,18 +1,19 @@ prefix.'prices') + DB::table($this->prefix.'prices') ->where('priceable_type', '=', \Lunar\Shipping\Models\ShippingRate::class) ->update([ 'priceable_type' => 'shipping_rate', ]); - \Illuminate\Support\Facades\DB::table($this->prefix.'shipping_exclusions') + DB::table($this->prefix.'shipping_exclusions') ->where('purchasable_type', '=', \Lunar\Models\Product::class) ->update([ 'purchasable_type' => 'product', @@ -21,16 +22,16 @@ public function up() public function down() { - \Illuminate\Support\Facades\DB::table($this->prefix.'prices') + DB::table($this->prefix.'prices') ->where('priceable_type', '=', 'shipping_rate') ->update([ 'priceable_type' => \Lunar\Shipping\Models\ShippingRate::class, ]); - \Illuminate\Support\Facades\DB::table($this->prefix.'shipping_exclusions') + DB::table($this->prefix.'shipping_exclusions') ->where('purchasable_type', '=', 'product') ->update([ 'purchasable_type' => \Lunar\Models\Product::class, ]); } -} +}; diff --git a/packages/table-rate-shipping/database/migrations/2024_06_28_100000_create_customer_group_shipping_method_table.php b/packages/table-rate-shipping/database/migrations/2024_06_28_100000_create_customer_group_shipping_method_table.php index 66bb1186c9..8ed215f29a 100644 --- a/packages/table-rate-shipping/database/migrations/2024_06_28_100000_create_customer_group_shipping_method_table.php +++ b/packages/table-rate-shipping/database/migrations/2024_06_28_100000_create_customer_group_shipping_method_table.php @@ -4,7 +4,7 @@ use Illuminate\Support\Facades\Schema; use Lunar\Base\Migration; -class CreateCustomerGroupShippingMethodTable extends Migration +return new class extends Migration { public function up() { @@ -26,4 +26,4 @@ public function down() { Schema::dropIfExists($this->prefix.'customer_group_shipping_method'); } -} +}; diff --git a/packages/table-rate-shipping/database/migrations/2025_08_18_100000_switch_shipping_to_jsonb_columns.php b/packages/table-rate-shipping/database/migrations/2025_08_18_100000_switch_shipping_to_jsonb_columns.php new file mode 100644 index 0000000000..6f49544b57 --- /dev/null +++ b/packages/table-rate-shipping/database/migrations/2025_08_18_100000_switch_shipping_to_jsonb_columns.php @@ -0,0 +1,33 @@ +prefix.'shipping_methods', function (Blueprint $table) { + $table->jsonb('data')->nullable()->change(); + }); + } + + public function down() + { + // Only run for PostgreSQL + if (DB::getDriverName() !== 'pgsql') { + return; + } + + Schema::table($this->prefix.'shipping_methods', function (Blueprint $table) { + $table->json('data')->nullable()->change(); + }); + } +}; diff --git a/packages/table-rate-shipping/phpunit.xml b/packages/table-rate-shipping/phpunit.xml deleted file mode 100644 index 1b64e22fb6..0000000000 --- a/packages/table-rate-shipping/phpunit.xml +++ /dev/null @@ -1,27 +0,0 @@ - -