├── js
├── admin.ts
├── dist-typings
│ ├── admin
│ │ └── index.d.ts
│ ├── common
│ │ ├── addModels.d.ts
│ │ ├── helpers
│ │ │ └── formatPrice.d.ts
│ │ ├── components
│ │ │ ├── QuantityInput.d.ts
│ │ │ ├── QuantityLabel.d.ts
│ │ │ ├── PriceInput.d.ts
│ │ │ ├── PriceLabel.d.ts
│ │ │ ├── OrderSortDropdown.d.ts
│ │ │ ├── ProductSortDropdown.d.ts
│ │ │ ├── DecimalLabel.d.ts
│ │ │ └── DecimalInput.d.ts
│ │ ├── states
│ │ │ ├── OrderListState.d.ts
│ │ │ └── ProductListState.d.ts
│ │ ├── models
│ │ │ ├── Payment.d.ts
│ │ │ ├── Cart.d.ts
│ │ │ ├── OrderLine.d.ts
│ │ │ ├── Order.d.ts
│ │ │ └── Product.d.ts
│ │ └── compat.d.ts
│ ├── backoffice
│ │ ├── addNavLinks.d.ts
│ │ ├── addRoutes.d.ts
│ │ ├── addUserListColumns.d.ts
│ │ ├── registerSettings.d.ts
│ │ ├── index.d.ts
│ │ ├── utils
│ │ │ ├── augmentOptionsWithValue.d.ts
│ │ │ └── OrderLineOptions.d.ts
│ │ ├── components
│ │ │ ├── OrderPaymentSection.d.ts
│ │ │ ├── OrderList.d.ts
│ │ │ ├── PaymentList.d.ts
│ │ │ ├── ProductList.d.ts
│ │ │ ├── OrderRelationshipSelect.d.ts
│ │ │ ├── ProductRelationshipSelect.d.ts
│ │ │ ├── EditPaymentModal.d.ts
│ │ │ └── OrderLineEdit.d.ts
│ │ ├── pages
│ │ │ ├── OrderIndexPage.d.ts
│ │ │ ├── ProductIndexPage.d.ts
│ │ │ ├── ProductShowPage.d.ts
│ │ │ └── OrderShowPage.d.ts
│ │ ├── states
│ │ │ ├── PaymentListPassthroughState.d.ts
│ │ │ └── OrderLineEditState.d.ts
│ │ └── compat.d.ts
│ ├── forum
│ │ ├── routes.d.ts
│ │ ├── index.d.ts
│ │ ├── utils
│ │ │ ├── DateFormat.d.ts
│ │ │ └── AccountControls.d.ts
│ │ ├── states
│ │ │ ├── ProductGridListState.d.ts
│ │ │ └── CartState.d.ts
│ │ ├── pages
│ │ │ ├── CartPage.d.ts
│ │ │ ├── AccountPage.d.ts
│ │ │ ├── AbstractAccountPage.d.ts
│ │ │ ├── OrderIndexPage.d.ts
│ │ │ ├── OrderShowPage.d.ts
│ │ │ ├── ProductIndexPage.d.ts
│ │ │ ├── AbstractShopPage.d.ts
│ │ │ └── ProductShowPage.d.ts
│ │ ├── components
│ │ │ ├── BrowsingDisabled.d.ts
│ │ │ ├── Breadcrumb.d.ts
│ │ │ ├── CartPageSection.d.ts
│ │ │ ├── CartList.d.ts
│ │ │ ├── ProductSearchSource.d.ts
│ │ │ ├── ProductQuantity.d.ts
│ │ │ ├── ProductListItem.d.ts
│ │ │ ├── InlineSubmitStatus.d.ts
│ │ │ ├── OrderFactPayment.d.ts
│ │ │ ├── OrderTableGroupHead.d.ts
│ │ │ ├── CartTable.d.ts
│ │ │ ├── OrderTableRow.d.ts
│ │ │ ├── OrderTableGroupFoot.d.ts
│ │ │ ├── OrderFact.d.ts
│ │ │ ├── OrderTable.d.ts
│ │ │ ├── OrderFacts.d.ts
│ │ │ ├── CartDropdown.d.ts
│ │ │ └── CartTableRow.d.ts
│ │ └── layouts
│ │ │ ├── AccountLayout.d.ts
│ │ │ ├── AbstractAccountLayout.d.ts
│ │ │ ├── OrderShowLayout.d.ts
│ │ │ ├── OrderIndexLayout.d.ts
│ │ │ ├── ProductIndexLayout.d.ts
│ │ │ ├── AbstractShopLayout.d.ts
│ │ │ └── ProductShowLayout.d.ts
│ └── mithril2html
│ │ ├── index.d.ts
│ │ ├── compat.d.ts
│ │ └── pages
│ │ ├── OrderSummary.d.ts
│ │ └── ProductSummary.d.ts
├── forum.ts
├── backoffice.ts
├── mithril2html.ts
├── package.json
├── tsconfig.json
├── dist
│ ├── admin.js
│ ├── mithril2html.js
│ └── admin.js.map
├── webpack.config.js
└── shims.d.ts
├── resources
└── less
│ ├── forum
│ ├── CartDropdown.less
│ ├── InlineSubmitStatus.less
│ ├── CartPage.less
│ ├── ProductList.less
│ └── OrderFacts.less
│ ├── backoffice.less
│ ├── common
│ └── DecimalInput.less
│ ├── backoffice
│ └── OrderComposerTable.less
│ └── forum.less
├── views
├── frontend
│ └── content
│ │ ├── product.blade.php
│ │ └── products.blade.php
└── email
│ ├── orderUpdated.blade.php
│ └── template.blade.php
├── src
├── Order
│ ├── Access
│ │ └── OrderPolicy.php
│ ├── Event
│ │ ├── Hidden.php
│ │ ├── Created.php
│ │ ├── Deleted.php
│ │ ├── Restored.php
│ │ ├── Deleting.php
│ │ ├── Saving.php
│ │ ├── UserChanged.php
│ │ ├── SavingLine.php
│ │ ├── Ordering.php
│ │ └── Paying.php
│ ├── Scope
│ │ ├── Enumerate.php
│ │ └── View.php
│ ├── OrderValidator.php
│ ├── OrderFilterer.php
│ ├── OrderSearcher.php
│ ├── IdSlugDriver.php
│ ├── UidSlugDriver.php
│ ├── Gambit
│ │ ├── FullTextGambit.php
│ │ └── UserGambit.php
│ ├── OrderServiceProvider.php
│ ├── OrderLine.php
│ ├── OrderBuilder.php
│ ├── OrderLineValidator.php
│ └── Order.php
├── Api
│ ├── Serializer
│ │ ├── UidSerializerTrait.php
│ │ ├── BasicOrderSerializer.php
│ │ ├── BasicProductSerializer.php
│ │ ├── PaymentSerializer.php
│ │ ├── OrderLineSerializer.php
│ │ ├── CartSerializer.php
│ │ ├── OrderSerializer.php
│ │ └── ProductSerializer.php
│ └── Controller
│ │ ├── OrderDeleteController.php
│ │ ├── PaymentDeleteController.php
│ │ ├── ProductDeleteController.php
│ │ ├── ProductStoreController.php
│ │ ├── CartUpdateController.php
│ │ ├── PaymentUpdateController.php
│ │ ├── OrderUpdateController.php
│ │ ├── PaymentStoreController.php
│ │ ├── CartSessionController.php
│ │ ├── OrderShowController.php
│ │ ├── ProductShowController.php
│ │ ├── ProductUpdateController.php
│ │ ├── OrderStoreController.php
│ │ ├── OrderIndexController.php
│ │ └── ProductIndexController.php
├── Product
│ ├── Scope
│ │ ├── Enumerate.php
│ │ └── View.php
│ ├── Event
│ │ ├── Hidden.php
│ │ ├── Created.php
│ │ ├── Deleted.php
│ │ ├── Restored.php
│ │ ├── Saving.php
│ │ ├── Deleting.php
│ │ ├── Renamed.php
│ │ └── DescriptionChanged.php
│ ├── Contract
│ │ ├── PriceDriverInterface.php
│ │ └── AvailabilityDriverInterface.php
│ ├── PriceDriver.php
│ ├── Access
│ │ └── ProductPolicy.php
│ ├── AvailabilityDriver
│ │ ├── AlwaysAvailable.php
│ │ └── NeverAvailable.php
│ ├── ProductFilterer.php
│ ├── ProductSearcher.php
│ ├── IdSlugDriver.php
│ ├── UidSlugDriver.php
│ ├── ProductValidator.php
│ ├── UserState.php
│ ├── Gambit
│ │ ├── FullTextGambit.php
│ │ └── TypeGambit.php
│ ├── CartState.php
│ ├── AbstractManager.php
│ ├── AvailabilityManager.php
│ ├── ProductServiceProvider.php
│ └── PriceManager.php
├── Cart
│ ├── Event
│ │ ├── Created.php
│ │ ├── Saving.php
│ │ ├── ProductQuantityUpdated.php
│ │ ├── UpdatingProductQuantity.php
│ │ └── WillOrder.php
│ ├── GuestCart.php
│ ├── Access
│ │ ├── ScopeCartVisibility.php
│ │ └── CartPolicy.php
│ ├── CartMiddleware.php
│ ├── Cart.php
│ └── CartLock.php
├── Payment
│ ├── Event
│ │ ├── Deleted.php
│ │ ├── Created.php
│ │ ├── Saving.php
│ │ └── Deleting.php
│ ├── PaymentValidator.php
│ └── Payment.php
├── Listener
│ ├── EnsureCartNotEmpty.php
│ ├── SendOrderConfirmation.php
│ ├── SaveUser.php
│ └── UpdateUserOrderMeta.php
├── UserAttributes.php
├── LoadForumCartRelationship.php
├── Notification
│ ├── OrderReceivedBlueprint.php
│ ├── MailConfig.php
│ └── AbstractOrderUpdateBlueprint.php
├── ForumAttributes.php
├── Mithril2Html
│ ├── ProductComponent.php
│ └── OrderComponent.php
├── Database
│ └── HasUid.php
├── Forum
│ └── Content
│ │ ├── Product.php
│ │ ├── Order.php
│ │ └── Products.php
└── Extend
│ ├── Price.php
│ ├── Availability.php
│ ├── Mail.php
│ └── Payment.php
├── migrations
├── 20210326_000700_alter_users_add_order_count.php
├── 20230117_000200_alter_order_payments_use_signed_int.php
├── 20230117_000000_alter_orders_use_signed_int.php
├── 20230125_000000_alter_carts_add_price_total_without_partial.php
├── 20230117_000100_alter_order_lines_use_signed_int.php
├── 20210326_000600_create_product_user_table.php
├── 20210326_000200_create_cart_product_table.php
├── 20210326_000000_create_products_table.php
├── 20210326_000300_create_orders_table.php
├── 20210326_000100_create_carts_table.php
├── 20210326_000500_create_order_payments_table.php
└── 20210326_000400_create_order_lines_table.php
├── .editorconfig
├── README.md
├── LICENSE.txt
└── composer.json
/js/admin.ts:
--------------------------------------------------------------------------------
1 | export * from './src/admin';
2 |
--------------------------------------------------------------------------------
/js/dist-typings/admin/index.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/js/forum.ts:
--------------------------------------------------------------------------------
1 | export * from './src/forum';
2 |
--------------------------------------------------------------------------------
/js/backoffice.ts:
--------------------------------------------------------------------------------
1 | export * from './src/backoffice';
2 |
--------------------------------------------------------------------------------
/js/mithril2html.ts:
--------------------------------------------------------------------------------
1 | export * from './src/mithril2html';
2 |
--------------------------------------------------------------------------------
/js/dist-typings/common/addModels.d.ts:
--------------------------------------------------------------------------------
1 | export default function (): void;
2 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/addNavLinks.d.ts:
--------------------------------------------------------------------------------
1 | export default function (): void;
2 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/addRoutes.d.ts:
--------------------------------------------------------------------------------
1 | export default function (): void;
2 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/routes.d.ts:
--------------------------------------------------------------------------------
1 | export default function (app: any): void;
2 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/addUserListColumns.d.ts:
--------------------------------------------------------------------------------
1 | export default function (): void;
2 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/registerSettings.d.ts:
--------------------------------------------------------------------------------
1 | export default function (): void;
2 |
--------------------------------------------------------------------------------
/resources/less/forum/CartDropdown.less:
--------------------------------------------------------------------------------
1 | .CartDropdownList {
2 | padding: 0 10px;
3 | }
4 |
--------------------------------------------------------------------------------
/resources/less/backoffice.less:
--------------------------------------------------------------------------------
1 | @import 'common/DecimalInput';
2 | @import 'backoffice/OrderComposerTable';
3 |
--------------------------------------------------------------------------------
/js/dist-typings/mithril2html/index.d.ts:
--------------------------------------------------------------------------------
1 | import { mithril2html } from './compat';
2 | export { mithril2html, };
3 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/index.d.ts:
--------------------------------------------------------------------------------
1 | import { common } from '../common/compat';
2 | import { forum } from './compat';
3 | export { common, forum, };
4 |
--------------------------------------------------------------------------------
/js/dist-typings/common/helpers/formatPrice.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @deprecated replaced with PriceLabel
3 | */
4 | export default function (price: any): any[];
5 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/index.d.ts:
--------------------------------------------------------------------------------
1 | import { common } from '../common/compat';
2 | import { backoffice } from './compat';
3 | export { common, backoffice, };
4 |
--------------------------------------------------------------------------------
/js/dist-typings/common/components/QuantityInput.d.ts:
--------------------------------------------------------------------------------
1 | import DecimalInput from './DecimalInput';
2 | export default class QuantityInput extends DecimalInput {
3 | }
4 |
--------------------------------------------------------------------------------
/js/dist-typings/common/components/QuantityLabel.d.ts:
--------------------------------------------------------------------------------
1 | import DecimalLabel from './DecimalLabel';
2 | export default class QuantityLabel extends DecimalLabel {
3 | }
4 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/utils/DateFormat.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: {
2 | defaultFormat(): string;
3 | orderDayFormat(): string;
4 | paymentDayFormat(): string;
5 | };
6 | export default _default;
7 |
--------------------------------------------------------------------------------
/views/frontend/content/product.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
{{ $apiDocument->data->attributes->title }}
3 |
4 | {!! $apiDocument->data->attributes->descriptionHtml !!}
5 |
6 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/utils/augmentOptionsWithValue.d.ts:
--------------------------------------------------------------------------------
1 | export default function (options: {
2 | [key: string]: string;
3 | }, value: string | undefined | null): {
4 | [key: string]: string;
5 | };
6 |
--------------------------------------------------------------------------------
/src/Order/Access/OrderPolicy.php:
--------------------------------------------------------------------------------
1 | ['integer', 'unsigned' => true, 'default' => 0],
7 | ]);
8 |
--------------------------------------------------------------------------------
/src/Api/Serializer/UidSerializerTrait.php:
--------------------------------------------------------------------------------
1 | uid;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/pages/CartPage.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import Page from 'flarum/common/components/Page';
3 | export default class CartPage extends Page {
4 | oninit(vnode: Vnode): void;
5 | view(): Mithril.Vnode;
6 | }
7 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/pages/AccountPage.d.ts:
--------------------------------------------------------------------------------
1 | import { VnodeDOM } from 'mithril';
2 | import Page from 'flarum/common/components/Page';
3 | export default class AccountPage extends Page {
4 | oncreate(vnode: VnodeDOM): void;
5 | view(): Mithril.Vnode;
6 | }
7 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/utils/AccountControls.d.ts:
--------------------------------------------------------------------------------
1 | import User from 'flarum/common/models/User';
2 | import ItemList from 'flarum/common/utils/ItemList';
3 | declare const _default: {
4 | controls(user: User): ItemList;
5 | };
6 | export default _default;
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | [*]
3 | end_of_line = lf
4 | charset = utf-8
5 | trim_trailing_whitespace = true
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 4
9 | [*.md]
10 | indent_size = 2
11 | trim_trailing_whitespace = false
12 |
--------------------------------------------------------------------------------
/js/dist-typings/common/states/OrderListState.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractListState from 'flamarkt/backoffice/common/states/AbstractListState';
2 | import Order from '../models/Order';
3 | export default class OrderListState extends AbstractListState {
4 | type(): string;
5 | }
6 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/BrowsingDisabled.d.ts:
--------------------------------------------------------------------------------
1 | import Component from 'flarum/common/Component';
2 | import ItemList from 'flarum/common/utils/ItemList';
3 | export default class BrowsingDisabled extends Component {
4 | view(): any;
5 | items(): ItemList;
6 | }
7 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/utils/OrderLineOptions.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: {
2 | orderLineGroupOptions(): {
3 | [key: string]: string;
4 | };
5 | orderLineTypeOptions(): {
6 | [key: string]: string;
7 | };
8 | };
9 | export default _default;
10 |
--------------------------------------------------------------------------------
/js/dist-typings/common/states/ProductListState.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractListState from 'flamarkt/backoffice/common/states/AbstractListState';
2 | import Product from '../models/Product';
3 | export default class ProductListState extends AbstractListState {
4 | type(): string;
5 | }
6 |
--------------------------------------------------------------------------------
/resources/less/forum/InlineSubmitStatus.less:
--------------------------------------------------------------------------------
1 | .FlamarktInlineSubmitStatus {
2 | display: inline-block;
3 | width: 36px;
4 | text-align: center;
5 |
6 | &.success {
7 | color: green;
8 | }
9 |
10 | &.error {
11 | color: red;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/js/dist-typings/mithril2html/compat.d.ts:
--------------------------------------------------------------------------------
1 | export const mithril2html: {
2 | 'pages/OrderSummary': typeof OrderSummary;
3 | 'pages/ProductSummary': typeof ProductSummary;
4 | };
5 | import { OrderSummary } from "./pages/OrderSummary";
6 | import { ProductSummary } from "./pages/ProductSummary";
7 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/pages/AbstractAccountPage.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractShopPage from './AbstractShopPage';
2 | /**
3 | * @deprecated replaced by Layout
4 | */
5 | export default abstract class AbstractAccountPage extends AbstractShopPage {
6 | breadcrumbItems(): import("flarum/common/utils/ItemList").default;
7 | }
8 |
--------------------------------------------------------------------------------
/src/Product/Scope/Enumerate.php:
--------------------------------------------------------------------------------
1 | where('user_id', $actor->id);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Order/Event/Deleting.php:
--------------------------------------------------------------------------------
1 | extends DecimalLabel {
6 | decimals(): number;
7 | view(): any;
8 | }
9 |
--------------------------------------------------------------------------------
/js/dist-typings/common/models/Payment.d.ts:
--------------------------------------------------------------------------------
1 | import Model from 'flarum/common/Model';
2 | export default class Payment extends Model {
3 | method: () => string | null;
4 | identifier: () => string | null;
5 | amount: () => number | null;
6 | createdAt: () => Date | null | undefined;
7 | isHidden: () => boolean;
8 | apiEndpoint(): string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/Payment/Event/Saving.php:
--------------------------------------------------------------------------------
1 | ;
5 | }
6 | export default class Breadcrumb extends Component {
7 | view(): any;
8 | items(): ItemList;
9 | }
10 | export {};
11 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/layouts/AccountLayout.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import AbstractAccountLayout from './AbstractAccountLayout';
3 | export default class AccountLayout extends AbstractAccountLayout {
4 | className(): string;
5 | title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
6 | currentPageHref(): string;
7 | content(): any;
8 | }
9 |
--------------------------------------------------------------------------------
/src/Product/Contract/AvailabilityDriverInterface.php:
--------------------------------------------------------------------------------
1 | extends AbstractShopLayout {
5 | breadcrumbItems(): import("flarum/common/utils/ItemList").default;
6 | }
7 |
--------------------------------------------------------------------------------
/js/dist-typings/mithril2html/pages/OrderSummary.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import Page from 'flarum/common/components/Page';
3 | import Order from '../../common/models/Order';
4 | import ItemList from 'flarum/common/utils/ItemList';
5 | export declare class OrderSummary extends Page {
6 | order: Order | null;
7 | oninit(vnode: Vnode): void;
8 | view(): any;
9 | sections(): ItemList;
10 | }
11 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/CartPageSection.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import Component, { ComponentAttrs } from 'flarum/common/Component';
3 | interface CartPageSectionAttrs extends ComponentAttrs {
4 | className: string;
5 | title: string;
6 | }
7 | export default class CartPageSection extends Component {
8 | view(vnode: Vnode): any;
9 | }
10 | export {};
11 |
--------------------------------------------------------------------------------
/js/dist-typings/mithril2html/pages/ProductSummary.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import Page from 'flarum/common/components/Page';
3 | import Product from '../../common/models/Product';
4 | import ItemList from 'flarum/common/utils/ItemList';
5 | export declare class ProductSummary extends Page {
6 | product: Product | null;
7 | oninit(vnode: Vnode): void;
8 | view(): any;
9 | sections(): ItemList;
10 | }
11 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/components/OrderPaymentSection.d.ts:
--------------------------------------------------------------------------------
1 | import Component from 'flarum/common/Component';
2 | import ItemList from 'flarum/common/utils/ItemList';
3 | import Order from '../../common/models/Order';
4 | export interface OrderPaymentSectionAttrs {
5 | order: Order;
6 | }
7 | export default class OrderPaymentSection extends Component {
8 | view(): any;
9 | fields(): ItemList;
10 | }
11 |
--------------------------------------------------------------------------------
/src/Order/Scope/Enumerate.php:
--------------------------------------------------------------------------------
1 | isGuest()) {
15 | $query->whereRaw('1=0');
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Order/Event/SavingLine.php:
--------------------------------------------------------------------------------
1 | price;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/pages/AbstractShopPage.d.ts:
--------------------------------------------------------------------------------
1 | import { Children } from 'mithril';
2 | import Page from 'flarum/common/components/Page';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | /**
5 | * @deprecated replaced by Layout
6 | */
7 | export default abstract class AbstractShopPage extends Page {
8 | view(): any;
9 | sidebarItems(): ItemList;
10 | breadcrumbItems(): ItemList;
11 | abstract content(): Children;
12 | }
13 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/components/OrderList.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractList from 'flamarkt/backoffice/backoffice/components/AbstractList';
2 | import Order from '../../common/models/Order';
3 | export default class OrderList extends AbstractList {
4 | head(): import("flarum/common/utils/ItemList").default;
5 | columns(order: Order): import("flarum/common/utils/ItemList").default;
6 | actions(order: Order): import("flarum/common/utils/ItemList").default;
7 | }
8 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/pages/OrderIndexPage.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import Page from 'flarum/common/components/Page';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import OrderListState from '../../common/states/OrderListState';
5 | export default class OrderIndexPage extends Page {
6 | list: OrderListState;
7 | oninit(vnode: Vnode): void;
8 | initState(): OrderListState;
9 | filters(): ItemList;
10 | view(): any;
11 | }
12 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/pages/ProductShowPage.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import AbstractShowPage from 'flamarkt/backoffice/common/pages/AbstractShowPage';
3 | import Product from '../../common/models/Product';
4 | export default class ProductShowPage extends AbstractShowPage {
5 | product: Product | null;
6 | oninit(vnode: Vnode): void;
7 | findType(): string;
8 | requestParams(): any;
9 | show(product: Product): void;
10 | view(): Mithril.Vnode;
11 | }
12 |
--------------------------------------------------------------------------------
/src/Listener/EnsureCartNotEmpty.php:
--------------------------------------------------------------------------------
1 | cart->products->isEmpty()) {
13 | throw new ValidationException([
14 | 'products' => 'Cart is empty',
15 | ]);
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/components/PaymentList.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractList from 'flamarkt/backoffice/backoffice/components/AbstractList';
2 | import Payment from '../../common/models/Payment';
3 | export default class PaymentList extends AbstractList {
4 | head(): import("flarum/common/utils/ItemList").default;
5 | columns(payment: Payment): import("flarum/common/utils/ItemList").default;
6 | actions(payment: Payment): import("flarum/common/utils/ItemList").default;
7 | }
8 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/components/ProductList.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractList from 'flamarkt/backoffice/backoffice/components/AbstractList';
2 | import Product from '../../common/models/Product';
3 | export default class ProductList extends AbstractList {
4 | head(): import("flarum/common/utils/ItemList").default;
5 | columns(product: Product): import("flarum/common/utils/ItemList").default;
6 | actions(product: Product): import("flarum/common/utils/ItemList").default;
7 | }
8 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/pages/ProductIndexPage.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import Page from 'flarum/common/components/Page';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import ProductListState from '../../common/states/ProductListState';
5 | export default class ProductIndexPage extends Page {
6 | list: ProductListState;
7 | oninit(vnode: Vnode): void;
8 | initState(): ProductListState;
9 | filters(): ItemList;
10 | view(): any;
11 | }
12 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/components/OrderRelationshipSelect.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractRelationshipSelect from 'flamarkt/backoffice/common/components/AbstractRelationshipSelect';
2 | import Order from '../../common/models/Order';
3 | export default class OrderRelationshipSelect extends AbstractRelationshipSelect {
4 | protected resultsCache: Map;
5 | search(query: string): Promise;
6 | results(query: string): Order[] | null;
7 | item(order: Order, query?: string): any[];
8 | }
9 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/states/PaymentListPassthroughState.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractListState from 'flamarkt/backoffice/common/states/AbstractListState';
2 | import Payment from '../../common/models/Payment';
3 | import Order from '../../common/models/Order';
4 | /**
5 | * Allows using a List component with the relationship from the order object
6 | */
7 | export default class PaymentListPassthroughState extends AbstractListState {
8 | type(): string;
9 | constructor(order: Order);
10 | }
11 |
--------------------------------------------------------------------------------
/src/UserAttributes.php:
--------------------------------------------------------------------------------
1 | getActor()->cannot('backoffice')) {
13 | return [];
14 | }
15 |
16 | return [
17 | 'flamarktOrderCount' => (int)$user->flamarkt_order_count,
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/js/dist-typings/common/components/OrderSortDropdown.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractSortDropdown, { SortDropdownAttrs, SortOptions } from 'flamarkt/backoffice/common/components/AbstractSortDropdown';
2 | import OrderListState from '../states/OrderListState';
3 | export interface OrderSortDropdownAttrs extends SortDropdownAttrs {
4 | list: OrderListState;
5 | }
6 | export default class OrderSortDropdown extends AbstractSortDropdown {
7 | className(): string;
8 | options(): SortOptions;
9 | }
10 |
--------------------------------------------------------------------------------
/js/dist-typings/common/models/Cart.d.ts:
--------------------------------------------------------------------------------
1 | import Model from 'flarum/common/Model';
2 | import Product from './Product';
3 | export default class Cart extends Model {
4 | productCount: () => number;
5 | priceTotal: () => number;
6 | amountDueAfterPartial: () => number;
7 | canAddProducts: () => boolean;
8 | canCheckout: () => boolean;
9 | isLocked: () => boolean;
10 | products: () => false | (Product | undefined)[];
11 | priceTotalLocal(): number;
12 | apiEndpoint(): string;
13 | }
14 |
--------------------------------------------------------------------------------
/js/dist-typings/common/models/OrderLine.d.ts:
--------------------------------------------------------------------------------
1 | import Model from 'flarum/common/Model';
2 | import Product from './Product';
3 | export default class OrderLine extends Model {
4 | number: () => number;
5 | group: () => string | null;
6 | type: () => string | null;
7 | label: () => string | null;
8 | comment: () => string | null;
9 | quantity: () => number | null;
10 | priceUnit: () => number | null;
11 | priceTotal: () => number | null;
12 | product: () => false | Product;
13 | }
14 |
--------------------------------------------------------------------------------
/src/Cart/Event/ProductQuantityUpdated.php:
--------------------------------------------------------------------------------
1 | {
8 | attrs: CartListAttrs;
9 | view(): any;
10 | showCartContents(): boolean;
11 | items(): ItemList;
12 | }
13 | export {};
14 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/components/ProductRelationshipSelect.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractRelationshipSelect from 'flamarkt/backoffice/common/components/AbstractRelationshipSelect';
2 | import Product from '../../common/models/Product';
3 | export default class ProductRelationshipSelect extends AbstractRelationshipSelect {
4 | protected resultsCache: Map;
5 | search(query: string): Promise;
6 | results(query: string): Product[] | null;
7 | item(product: Product, query?: string): any[];
8 | }
9 |
--------------------------------------------------------------------------------
/js/dist-typings/common/components/ProductSortDropdown.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractSortDropdown, { SortDropdownAttrs, SortOptions } from 'flamarkt/backoffice/common/components/AbstractSortDropdown';
2 | import ProductListState from '../states/ProductListState';
3 | export interface ProductSortDropdownAttrs extends SortDropdownAttrs {
4 | list: ProductListState;
5 | }
6 | export default class ProductSortDropdown extends AbstractSortDropdown {
7 | className(): string;
8 | options(): SortOptions;
9 | }
10 |
--------------------------------------------------------------------------------
/migrations/20230117_000200_alter_order_payments_use_signed_int.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->table('flamarkt_order_payments', function (Blueprint $table) {
9 | $table->integer('amount')->change();
10 | });
11 | },
12 | 'down' => function (Builder $schema) {
13 | // Not implemented since Flarum doesn't support partial rollbacks
14 | },
15 | ];
16 |
--------------------------------------------------------------------------------
/src/Order/Event/Ordering.php:
--------------------------------------------------------------------------------
1 | ;
7 | search(query: string): Promise;
8 | view(query: string): Array;
9 | product(product: Product, query: string): ItemList;
10 | }
11 |
--------------------------------------------------------------------------------
/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@flamarkt/core",
3 | "private": true,
4 | "dependencies": {
5 | "flarum-tsconfig": "^1.0",
6 | "flarum-webpack-config": "^2.0",
7 | "webpack": "^5.65",
8 | "webpack-cli": "^4.9"
9 | },
10 | "scripts": {
11 | "dev": "webpack --mode development --watch",
12 | "build": "webpack --mode production",
13 | "clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
14 | "build-typings": "npm run clean-typings && tsc"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Cart/Event/UpdatingProductQuantity.php:
--------------------------------------------------------------------------------
1 | {
8 | cartQuantity: number;
9 | savingQuantity: boolean;
10 | oninit(vnode: Vnode): void;
11 | view(): any;
12 | }
13 | export {};
14 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/ProductListItem.d.ts:
--------------------------------------------------------------------------------
1 | import { Children } from 'mithril';
2 | import Component, { ComponentAttrs } from 'flarum/common/Component';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import Product from '../../common/models/Product';
5 | interface ProductListItemAttrs extends ComponentAttrs {
6 | product: Product;
7 | }
8 | export default class ProductListItem extends Component {
9 | view(): any;
10 | link(children: Children): any;
11 | items(): ItemList;
12 | }
13 | export {};
14 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/InlineSubmitStatus.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import Component, { ComponentAttrs } from 'flarum/common/Component';
3 | export declare type InlineSubmitStatusResult = 'success' | 'error' | null;
4 | export interface InlineSubmitStatusAttrs extends ComponentAttrs {
5 | loading?: boolean;
6 | result?: InlineSubmitStatusResult;
7 | }
8 | export default class InlineSubmitStatus extends Component {
9 | view(vnode: Vnode): any;
10 | content(): any;
11 | }
12 |
--------------------------------------------------------------------------------
/src/Product/Access/ProductPolicy.php:
--------------------------------------------------------------------------------
1 | can('backoffice');
14 | }
15 |
16 | public function orderWhenAvailable(User $actor, Product $product)
17 | {
18 | return $actor->hasPermission('flamarkt.cart');
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/migrations/20230117_000000_alter_orders_use_signed_int.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->table('flamarkt_orders', function (Blueprint $table) {
9 | $table->integer('price_total')->change();
10 | $table->integer('paid_amount')->change();
11 | });
12 | },
13 | 'down' => function (Builder $schema) {
14 | // Not implemented since Flarum doesn't support partial rollbacks
15 | },
16 | ];
17 |
--------------------------------------------------------------------------------
/js/dist-typings/common/components/DecimalLabel.d.ts:
--------------------------------------------------------------------------------
1 | import { Children } from 'mithril';
2 | import Component, { ComponentAttrs } from 'flarum/common/Component';
3 | import Product from '../models/Product';
4 | export interface DecimalLabelAttrs extends ComponentAttrs {
5 | value: number;
6 | product?: Product;
7 | unit?: string;
8 | decimals?: number;
9 | }
10 | export default class DecimalLabel extends Component {
11 | decimals(): number;
12 | fromIntegerValue(value: number): number;
13 | view(): Children;
14 | }
15 |
--------------------------------------------------------------------------------
/src/Order/Event/Paying.php:
--------------------------------------------------------------------------------
1 | {
9 | view(): any;
10 | items(): ItemList;
11 | date(): Children;
12 | label(): Children;
13 | amount(): Children;
14 | }
15 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/states/OrderLineEditState.d.ts:
--------------------------------------------------------------------------------
1 | import OrderLine from '../../common/models/OrderLine';
2 | import Product from '../../common/models/Product';
3 | export default class OrderLineEditState {
4 | uniqueFormKey: string;
5 | line?: OrderLine;
6 | group: string | null;
7 | type: string | null;
8 | label: string;
9 | comment: string;
10 | quantity: number;
11 | priceUnit: number;
12 | priceTotal: number;
13 | product: Product | null;
14 | constructor();
15 | init(line: OrderLine): void;
16 | updateTotal(): void;
17 | data(): any;
18 | }
19 |
--------------------------------------------------------------------------------
/src/Product/AvailabilityDriver/AlwaysAvailable.php:
--------------------------------------------------------------------------------
1 | {
11 | view(): any;
12 | visible(): boolean;
13 | columns(): ItemList;
14 | }
15 | export {};
16 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/CartTable.d.ts:
--------------------------------------------------------------------------------
1 | import Component, { ComponentAttrs } from 'flarum/common/Component';
2 | import ItemList from 'flarum/common/utils/ItemList';
3 | import Cart from '../../common/models/Cart';
4 | import Product from '../../common/models/Product';
5 | interface CartTableAttrs extends ComponentAttrs {
6 | cart: Cart;
7 | }
8 | export default class CartTable extends Component {
9 | view(): any;
10 | head(): ItemList;
11 | rows(): ItemList;
12 | productRowKey(product: Product): string;
13 | productRowPriority(product: Product): number;
14 | }
15 | export {};
16 |
--------------------------------------------------------------------------------
/js/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "flarum-tsconfig",
3 | "include": [
4 | "src/**/*",
5 | "../vendor/flarum/core/js/dist-typings/@types/**/*"
6 | ],
7 | "files": [
8 | "shims.d.ts"
9 | ],
10 | "compilerOptions": {
11 | "declarationDir": "./dist-typings",
12 | "baseUrl": ".",
13 | "paths": {
14 | "flamarkt/backoffice/*": [
15 | "../vendor/flamarkt/backoffice/js/dist-typings/*"
16 | ],
17 | "flarum/*": [
18 | "../vendor/flarum/core/js/dist-typings/*"
19 | ]
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/OrderTableRow.d.ts:
--------------------------------------------------------------------------------
1 | import Component, { ComponentAttrs } from 'flarum/common/Component';
2 | import OrderLine from '../../common/models/OrderLine';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import Order from '../../common/models/Order';
5 | interface OrderTableRowAttrs extends ComponentAttrs {
6 | line: OrderLine;
7 | order: Order;
8 | }
9 | export default class OrderTableRow extends Component {
10 | view(): any;
11 | columns(): ItemList;
12 | productContent(): any[];
13 | labelContent(): any;
14 | commentContent(): any;
15 | }
16 | export {};
17 |
--------------------------------------------------------------------------------
/src/Listener/SendOrderConfirmation.php:
--------------------------------------------------------------------------------
1 | notifications->sync(
20 | new OrderReceivedBlueprint($event->order),
21 | [$event->order->user]
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/migrations/20230125_000000_alter_carts_add_price_total_without_partial.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->table('flamarkt_carts', function (Blueprint $table) {
9 | $table->integer('amount_due_after_partial')->nullable()->after('price_total');
10 | });
11 | },
12 | 'down' => function (Builder $schema) {
13 | $schema->table('flamarkt_carts', function (Blueprint $table) {
14 | $table->dropColumn('amount_due_after_partial');
15 | });
16 | },
17 | ];
18 |
--------------------------------------------------------------------------------
/src/LoadForumCartRelationship.php:
--------------------------------------------------------------------------------
1 | getAttribute('cart');
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/OrderTableGroupFoot.d.ts:
--------------------------------------------------------------------------------
1 | import Component, { ComponentAttrs } from 'flarum/common/Component';
2 | import OrderLine from '../../common/models/OrderLine';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import Order from '../../common/models/Order';
5 | interface OrderTableGroupFootAttrs extends ComponentAttrs {
6 | group: string;
7 | lines: OrderLine[];
8 | order: Order;
9 | }
10 | export default class OrderTableGroupFoot extends Component {
11 | view(): any;
12 | visible(): boolean;
13 | subtotal(): number;
14 | columns(): ItemList;
15 | }
16 | export {};
17 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/OrderFact.d.ts:
--------------------------------------------------------------------------------
1 | import { Children, Vnode } from 'mithril';
2 | import Component, { ComponentAttrs } from 'flarum/common/Component';
3 | export interface OrderFactAttrs extends ComponentAttrs {
4 | title: Children;
5 | className?: string;
6 | }
7 | /**
8 | * A component is used because it makes it easier to pass an optional className and provides better extensibility.
9 | * The component children should be pre-wrapped in .
10 | * The helper method OrderFacts.prototype.wrapContent() can be used to achieve that.
11 | */
12 | export default class OrderFact extends Component {
13 | view(vnode: Vnode): any;
14 | }
15 |
--------------------------------------------------------------------------------
/migrations/20230117_000100_alter_order_lines_use_signed_int.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->table('flamarkt_order_lines', function (Blueprint $table) {
9 | $table->integer('price_unit')->nullable()->change();
10 | $table->integer('quantity')->nullable()->change();
11 | $table->integer('price_total')->change();
12 | });
13 | },
14 | 'down' => function (Builder $schema) {
15 | // Not implemented since Flarum doesn't support partial rollbacks
16 | },
17 | ];
18 |
--------------------------------------------------------------------------------
/views/email/orderUpdated.blade.php:
--------------------------------------------------------------------------------
1 | @extends(\Flamarkt\Core\Notification\MailConfig::$templateView)
2 |
3 | @section('css')
4 | .CartTable {
5 | min-width: 100%;
6 | }
7 |
8 | .CartTable th, .CartTable td {
9 | border-collapse: collapse;
10 | text-align: left;
11 | padding: 10px 15px;
12 | }
13 |
14 | .CartTable thead th {
15 | border-bottom: 2px solid #666;
16 | }
17 |
18 | .CartTable tbody tr:nth-child(odd) {
19 | background: #f6f6f6;
20 | }
21 | @endsection
22 |
23 | @section('content')
24 | {!! $blueprint->getEmailMessage($translator) !!}
25 |
26 | @mithril2html($blueprint->getEmailComponent())
27 | @endsection
28 |
--------------------------------------------------------------------------------
/src/Order/OrderValidator.php:
--------------------------------------------------------------------------------
1 | order;
15 | }
16 |
17 | public function setOrder(Order $order)
18 | {
19 | $this->order = $order;
20 | }
21 |
22 | protected $rules = [
23 | 'userId' => [
24 | 'nullable',
25 | 'exists:users,id',
26 | ],
27 | ];
28 | }
29 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/layouts/OrderShowLayout.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import AbstractAccountLayout, { AbstractAccountLayoutAttrs } from './AbstractAccountLayout';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import Order from '../../common/models/Order';
5 | export interface OrderShowLayoutAttrs extends AbstractAccountLayoutAttrs {
6 | order?: Order;
7 | }
8 | export default class OrderShowLayout extends AbstractAccountLayout {
9 | className(): string;
10 | title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
11 | content(): any;
12 | sections(): ItemList;
13 | }
14 |
--------------------------------------------------------------------------------
/src/Order/OrderFilterer.php:
--------------------------------------------------------------------------------
1 | repository->visibleTo($actor, 'viewEnumerate');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/js/dist-typings/common/models/Order.d.ts:
--------------------------------------------------------------------------------
1 | import Model from 'flarum/common/Model';
2 | import User from 'flarum/common/models/User';
3 | import OrderLine from './OrderLine';
4 | import Payment from './Payment';
5 | export default class Order extends Model {
6 | number: () => string;
7 | slug: () => string;
8 | productCount: () => number;
9 | priceTotal: () => number;
10 | paidAmount: () => number;
11 | createdAt: () => Date | null | undefined;
12 | isHidden: () => boolean;
13 | user: () => false | User;
14 | lines: () => false | (OrderLine | undefined)[];
15 | payments: () => false | (Payment | undefined)[];
16 | titleDate(): string;
17 | apiEndpoint(): string;
18 | }
19 |
--------------------------------------------------------------------------------
/src/Product/ProductFilterer.php:
--------------------------------------------------------------------------------
1 | repository->visibleTo($actor, 'viewEnumerate');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/resources/less/forum/CartPage.less:
--------------------------------------------------------------------------------
1 | .CartPage-section {
2 | border: 2px solid #f6f6f6;
3 | border-radius: 5px;
4 | padding: 10px;
5 | margin-bottom: 10px;
6 | }
7 |
8 | .CartTable {
9 | min-width: 100%;
10 |
11 | th, td {
12 | border-collapse: collapse;
13 | text-align: left;
14 | padding: 10px 15px;
15 | }
16 |
17 | thead th {
18 | border-bottom: 2px solid #666;
19 | }
20 |
21 | tbody tr:nth-child(odd) {
22 | background: #f6f6f6;
23 | }
24 | }
25 |
26 | .CartTableQuantity {
27 | // We must use an inline style because the loading spinner is inserted next to the input
28 | display: inline-flex;
29 | width: 10em;
30 | }
31 |
--------------------------------------------------------------------------------
/src/Order/OrderSearcher.php:
--------------------------------------------------------------------------------
1 | repository->visibleTo($actor, 'viewEnumerate');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Order/IdSlugDriver.php:
--------------------------------------------------------------------------------
1 | id;
24 | }
25 |
26 | public function fromSlug(string $slug, User $actor): AbstractModel
27 | {
28 | return $this->orders->findIdOrFail($slug, $actor);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Order/UidSlugDriver.php:
--------------------------------------------------------------------------------
1 | uid;
24 | }
25 |
26 | public function fromSlug(string $slug, User $actor): AbstractModel
27 | {
28 | return $this->orders->findUidOrFail($slug, $actor);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Product/ProductSearcher.php:
--------------------------------------------------------------------------------
1 | repository->visibleTo($actor, 'viewEnumerate');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/OrderTable.d.ts:
--------------------------------------------------------------------------------
1 | import Component, { ComponentAttrs } from 'flarum/common/Component';
2 | import ItemList from 'flarum/common/utils/ItemList';
3 | import Order from '../../common/models/Order';
4 | import OrderLine from '../../common/models/OrderLine';
5 | interface OrderTableAttrs extends ComponentAttrs {
6 | order: Order;
7 | }
8 | export default class OrderTable extends Component {
9 | view(): any;
10 | head(): ItemList;
11 | foot(): ItemList;
12 | rows(): ItemList;
13 | lineKey(line: OrderLine): string;
14 | linePriority(line: OrderLine): number;
15 | lineHeadPriority(group: string): number;
16 | lineFootPriority(group: string): number;
17 | }
18 | export {};
19 |
--------------------------------------------------------------------------------
/src/Order/Scope/View.php:
--------------------------------------------------------------------------------
1 | can('backoffice')) {
13 | if ($actor->isGuest()) {
14 | // Officially guest orders are not yet implemented, but by having this rule here
15 | // And enumeration disabled for guests, this would allow any guest to hit any guest order with the correct UID
16 | $query->whereNull('user_id');
17 | } else {
18 | $query->where('user_id', $actor->id);
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Product/IdSlugDriver.php:
--------------------------------------------------------------------------------
1 | id;
24 | }
25 |
26 | public function fromSlug(string $slug, User $actor): AbstractModel
27 | {
28 | return $this->products->findIdOrFail($slug, $actor);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Product/UidSlugDriver.php:
--------------------------------------------------------------------------------
1 | uid;
24 | }
25 |
26 | public function fromSlug(string $slug, User $actor): AbstractModel
27 | {
28 | return $this->products->findUidOrFail($slug, $actor);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/migrations/20210326_000600_create_product_user_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->create('flamarkt_product_user', function (Blueprint $table) {
9 | $table->unsignedInteger('product_id');
10 | $table->unsignedInteger('user_id');
11 |
12 | $table->foreign('product_id')->references('id')->on('flamarkt_products')->onDelete('cascade');
13 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
14 | });
15 | },
16 | 'down' => function (Builder $schema) {
17 | $schema->dropIfExists('flamarkt_product_user');
18 | },
19 | ];
20 |
--------------------------------------------------------------------------------
/src/Product/Scope/View.php:
--------------------------------------------------------------------------------
1 | can('backoffice')) {
13 | // Return early for backoffice permission, there won't be any restriction
14 | return;
15 | }
16 |
17 | if (!$actor->hasPermission('flamarkt.browse')) {
18 | // Block access to catalog
19 | $query->whereRaw('0=1');
20 | return;
21 | }
22 |
23 | // For regular users with browse permission, show all non-hidden
24 | $query->whereNull('hidden_at');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Product/AvailabilityDriver/NeverAvailable.php:
--------------------------------------------------------------------------------
1 | {
10 | view(): any;
11 | items(): ItemList;
12 | shippingInformation(): ItemList;
13 | paymentInformation(): ItemList;
14 | paymentVisible(payment: Payment): boolean;
15 | dontWrapContent(vnode: Vnode): boolean;
16 | wrapContent(...content: Children[]): Children;
17 | }
18 |
--------------------------------------------------------------------------------
/resources/less/forum/ProductList.less:
--------------------------------------------------------------------------------
1 | .ProductList {
2 | display: flex;
3 | flex-wrap: wrap;
4 | list-style: none;
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | .ProductListItem {
10 | display: flex;
11 | margin-bottom: 20px;
12 | }
13 |
14 | .ProductListItem--link {
15 | display: block;
16 | width: 100%;
17 | padding: 10px 20px;
18 | color: inherit;
19 |
20 | &:hover {
21 | text-decoration: none;
22 | background: #fafafa;
23 | }
24 | }
25 |
26 | .ProductListItem--title {
27 | display: block;
28 | font-weight: bold;
29 | }
30 |
31 | @media @tablet-up {
32 | .ProductListItem {
33 | width: 30%;
34 |
35 | &:not(:nth-child(3n)) {
36 | border-right: 1px solid #dedede;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/resources/less/common/DecimalInput.less:
--------------------------------------------------------------------------------
1 | .FlamarktDecimalInput {
2 | padding: 0;
3 | display: flex;
4 |
5 | // Copied from .Form-control
6 | &.focused {
7 | background-color: var(--body-bg);
8 | color: var(--text-color);
9 | border-color: var(--primary-color);
10 | }
11 |
12 | input {
13 | display: block;
14 | padding: 8px 13px; // Same as .Form-control
15 | border: none;
16 | background: transparent;
17 | width: 100%;
18 | flex-grow: 1;
19 | flex-shrink: 1;
20 |
21 | &:focus {
22 | outline: none;
23 | }
24 | }
25 | }
26 |
27 | .FlamarktDecimalInputUnit {
28 | padding: 8px 13px 0 0; // Made to match with .Form-control
29 | line-height: 15px;
30 | cursor: text;
31 | }
32 |
--------------------------------------------------------------------------------
/js/dist-typings/common/models/Product.d.ts:
--------------------------------------------------------------------------------
1 | import Model from 'flarum/common/Model';
2 | export default class Product extends Model {
3 | title: () => string;
4 | slug: () => string;
5 | description: () => string | null;
6 | descriptionHtml: () => string | null;
7 | price: () => number | null;
8 | priceEdit: () => number | null;
9 | cartQuantity: () => number | null;
10 | isHidden: () => boolean;
11 | canAddToCart: () => number | null;
12 | /**
13 | * @deprecated Use canAddToCart instead. Behaviour will probably change in the future to differentiate products that can be added to cart but not actually ordered.
14 | */
15 | canOrder: () => number | null;
16 | canEdit: () => number | null;
17 | cartPriceTotalLocal(): number;
18 | apiEndpoint(): string;
19 | }
20 |
--------------------------------------------------------------------------------
/js/dist/admin.js:
--------------------------------------------------------------------------------
1 | (()=>{var e={n:t=>{var o=t&&t.__esModule?()=>t.default:()=>t;return e.d(o,{a:o}),o},d:(t,o)=>{for(var r in o)e.o(o,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:o[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};(()=>{"use strict";e.r(t);const o=flarum.core.compat["common/extend"],r=flarum.core.compat["admin/components/BasicsPage"];var a=e.n(r);app.initializers.add("flamarkt-core",(function(){(0,o.extend)(a().prototype,"homePageItems",(function(e){e.add("flamarkt-products",{path:"/products",label:app.translator.trans("flamarkt-core.admin.homepage.products")})}))}))})(),module.exports=t})();
2 | //# sourceMappingURL=admin.js.map
--------------------------------------------------------------------------------
/js/webpack.config.js:
--------------------------------------------------------------------------------
1 | const config = require('flarum-webpack-config')();
2 |
3 | config.entry = {
4 | admin: './admin.ts',
5 | backoffice: './backoffice.ts',
6 | forum: './forum.ts',
7 | mithril2html: './mithril2html.ts',
8 | };
9 |
10 | // We need to import through our external namespace because we need changes that the forum bundle makes on the components
11 | // This is also used for backoffice imports
12 | config.externals.push(function ({context, request}, callback) {
13 | let matches;
14 | if ((matches = /^(flamarkt\/[^/]+)\/([^/]+)\/(.+)$/.exec(request))) {
15 | return callback(null, 'root ((flarum.extensions[\'' + matches[1].replace('/', '-') + '\']||{})[\'' + matches[2] + '\']||{})[\'' + matches[3] + '\']');
16 | }
17 | callback();
18 | });
19 |
20 | module.exports = config;
21 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/components/EditPaymentModal.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import Modal from 'flarum/common/components/Modal';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import Payment from '../../common/models/Payment';
5 | interface EditPaymentModalAttrs {
6 | payment?: Payment;
7 | }
8 | export default class EditPaymentModal extends Modal {
9 | payment: Payment;
10 | method: string;
11 | identifier: string;
12 | amount: number;
13 | dirty: boolean;
14 | oninit(vnode: Vnode): void;
15 | content(): any;
16 | fields(): ItemList;
17 | data(): {
18 | method: string | null;
19 | identifier: string | null;
20 | amount: number;
21 | };
22 | onsubmit(event: Event): void;
23 | }
24 | export {};
25 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/CartDropdown.d.ts:
--------------------------------------------------------------------------------
1 | import { ComponentAttrs } from 'flarum/common/Component';
2 | import Dropdown from 'flarum/common/components/Dropdown';
3 | import CartState from '../states/CartState';
4 | interface CartDropdownAttrs extends ComponentAttrs {
5 | className: string;
6 | buttonClassName: string;
7 | menuClassName: string;
8 | label: string;
9 | icon: string;
10 | state: CartState;
11 | }
12 | export default class CartDropdown extends Dropdown {
13 | attrs: CartDropdownAttrs;
14 | static initAttrs(attrs: CartDropdownAttrs): void;
15 | getButton(children: any): any;
16 | getButtonContent(children: any): any[];
17 | getMenu(): any;
18 | onclick(event: Event): void;
19 | goToRoute(): void;
20 | menuClick(e: MouseEvent): void;
21 | }
22 | export {};
23 |
--------------------------------------------------------------------------------
/migrations/20210326_000200_create_cart_product_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->create('flamarkt_cart_product', function (Blueprint $table) {
9 | $table->unsignedInteger('cart_id');
10 | $table->unsignedInteger('product_id');
11 | $table->unsignedInteger('quantity');
12 |
13 | $table->foreign('cart_id')->references('id')->on('flamarkt_carts')->onDelete('cascade');
14 | $table->foreign('product_id')->references('id')->on('flamarkt_products')->onDelete('cascade');
15 | });
16 | },
17 | 'down' => function (Builder $schema) {
18 | $schema->dropIfExists('flamarkt_cart_product');
19 | },
20 | ];
21 |
--------------------------------------------------------------------------------
/src/Notification/OrderReceivedBlueprint.php:
--------------------------------------------------------------------------------
1 | trans('flamarkt-core.email.orderReceived.subject', [
17 | '{number}' => $this->order->id,
18 | ]);
19 | }
20 |
21 | public function getEmailMessage(TranslatorInterface $translator): string
22 | {
23 | return '' . htmlspecialchars($translator->trans('flamarkt-core.email.orderReceived.message')) . '
';
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Api/Serializer/BasicOrderSerializer.php:
--------------------------------------------------------------------------------
1 | $order->id,
29 | 'slug' => $this->slugManager->forResource(Order::class)->toSlug($order),
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/ForumAttributes.php:
--------------------------------------------------------------------------------
1 | 2,
13 | 'priceUnit' => 'CHF',
14 | 'flamarktCanBrowse' => $serializer->getActor()->hasPermission('flamarkt.browse'),
15 | ];
16 |
17 | if ($serializer->getActor()->can('backoffice')) {
18 | $attributes += [
19 | 'flamarktAvailabilityDrivers' => array_keys(resolve('flamarkt.availability_drivers')),
20 | 'flamarktPriceDrivers' => array_keys(resolve('flamarkt.price_drivers')),
21 | ];
22 | }
23 |
24 | return $attributes;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Mithril2Html/ProductComponent.php:
--------------------------------------------------------------------------------
1 | product->uid;
25 | }
26 |
27 | public function actor(): ?User
28 | {
29 | return null;
30 | }
31 |
32 | public function selector(): ?string
33 | {
34 | return '#content';
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/states/CartState.d.ts:
--------------------------------------------------------------------------------
1 | import Cart from '../../common/models/Cart';
2 | export default class CartState {
3 | cart: Cart | null;
4 | loading: boolean;
5 | priceTotal(): number | null;
6 | productCount(): number | null;
7 | /**
8 | * To be called by Flamarkt itself with the boot payload
9 | */
10 | boot(): void;
11 | /**
12 | * Separate method so that extensions can retrieve a cart update without triggering the loading status or redraw
13 | * But with all the same relationships.
14 | */
15 | request(): Promise;
16 | /**
17 | * Intended to be used with a manually retrieved value with request()
18 | * @param cart
19 | */
20 | setCart(cart: Cart | null): void;
21 | /**
22 | * Refresh the global cart
23 | */
24 | load(): Promise;
25 | }
26 |
--------------------------------------------------------------------------------
/src/Api/Serializer/BasicProductSerializer.php:
--------------------------------------------------------------------------------
1 | $product->title,
29 | 'slug' => $this->slugManager->forResource(Product::class)->toSlug($product),
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Product/ProductValidator.php:
--------------------------------------------------------------------------------
1 | product;
15 | }
16 |
17 | public function setProduct(Product $product)
18 | {
19 | $this->product = $product;
20 | }
21 |
22 | protected $rules = [
23 | 'title' => [
24 | 'required',
25 | 'string',
26 | 'min:1',
27 | 'max:255',
28 | ],
29 | 'description' => [
30 | 'nullable',
31 | 'string',
32 | 'max:65535',
33 | ],
34 | ];
35 | }
36 |
--------------------------------------------------------------------------------
/src/Api/Controller/OrderDeleteController.php:
--------------------------------------------------------------------------------
1 | repository->findUidOrFail(Arr::get($request->getQueryParams(), 'id'), $actor);
24 |
25 | $this->repository->delete($order, $actor, (array)Arr::get($request->getParsedBody(), 'data'));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Api/Controller/PaymentDeleteController.php:
--------------------------------------------------------------------------------
1 | repository->findUidOrFail(Arr::get($request->getQueryParams(), 'id'), $actor);
24 |
25 | $this->repository->delete($payment, $actor, (array)Arr::get($request->getParsedBody(), 'data'));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Api/Controller/ProductDeleteController.php:
--------------------------------------------------------------------------------
1 | repository->findUidOrFail(Arr::get($request->getQueryParams(), 'id'), $actor);
24 |
25 | $this->repository->delete($product, $actor, (array)Arr::get($request->getParsedBody(), 'data'));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Api/Controller/ProductStoreController.php:
--------------------------------------------------------------------------------
1 | repository->store(RequestUtil::getActor($request), (array)Arr::get($request->getParsedBody(), 'data'));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Cart/Event/WillOrder.php:
--------------------------------------------------------------------------------
1 | void;
9 | }
10 | export default class OrderLineEdit implements ClassComponent {
11 | view(vnode: Vnode): any;
12 | columns(line: OrderLineEditState, control: Vnode, onchange: () => void, sortable?: boolean): ItemList;
13 | showInfoProduct(line: OrderLineEditState): boolean;
14 | showInfoLabel(line: OrderLineEditState): boolean;
15 | showInfoComment(line: OrderLineEditState): boolean;
16 | fields(line: OrderLineEditState, onchange: () => void): ItemList;
17 | }
18 | export {};
19 |
--------------------------------------------------------------------------------
/src/Mithril2Html/OrderComponent.php:
--------------------------------------------------------------------------------
1 | notificationType;
21 | }
22 |
23 | public function preload(): ?string
24 | {
25 | return '/flamarkt/orders/' . $this->order->uid;
26 | }
27 |
28 | public function actor(): ?User
29 | {
30 | return $this->order->user;
31 | }
32 |
33 | public function selector(): ?string
34 | {
35 | return '#content';
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/migrations/20210326_000000_create_products_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->create('flamarkt_products', function (Blueprint $table) {
9 | $table->increments('id');
10 | $table->string('uid')->unique();
11 | $table->string('title')->nullable()->index();
12 | $table->text('description')->nullable();
13 | $table->unsignedInteger('price')->nullable();
14 | $table->string('availability_driver')->nullable();
15 | $table->string('price_driver')->nullable();
16 | $table->timestamps();
17 | $table->timestamp('hidden_at')->nullable();
18 | });
19 | },
20 | 'down' => function (Builder $schema) {
21 | $schema->dropIfExists('flamarkt_products');
22 | },
23 | ];
24 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/layouts/OrderIndexLayout.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Children } from 'mithril';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import AbstractAccountLayout, { AbstractAccountLayoutAttrs } from './AbstractAccountLayout';
5 | import OrderListState from '../../common/states/OrderListState';
6 | import Order from '../../common/models/Order';
7 | export interface OrderIndexLayoutAttrs extends AbstractAccountLayoutAttrs {
8 | state: OrderListState;
9 | }
10 | export default class OrderIndexLayout extends AbstractAccountLayout {
11 | className(): string;
12 | title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
13 | content(): Children;
14 | headerRow(): ItemList;
15 | orderRow(order: Order): ItemList;
16 | bottomRowContent(): Children;
17 | bottomRow(): Children;
18 | }
19 |
--------------------------------------------------------------------------------
/migrations/20210326_000300_create_orders_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->create('flamarkt_orders', function (Blueprint $table) {
9 | $table->increments('id');
10 | $table->string('uid')->unique();
11 | $table->unsignedInteger('user_id')->nullable();
12 | $table->unsignedInteger('price_total');
13 | $table->unsignedInteger('paid_amount');
14 | $table->unsignedInteger('product_count');
15 | $table->timestamps();
16 | $table->timestamp('hidden_at')->nullable();
17 |
18 | $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
19 | });
20 | },
21 | 'down' => function (Builder $schema) {
22 | $schema->dropIfExists('flamarkt_orders');
23 | },
24 | ];
25 |
--------------------------------------------------------------------------------
/resources/less/backoffice/OrderComposerTable.less:
--------------------------------------------------------------------------------
1 | .OrderComposerTable {
2 | width: 100%;
3 | border-collapse: collapse;
4 |
5 | th, td {
6 | padding: 5px;
7 | vertical-align: top;
8 | }
9 |
10 | th {
11 | text-align: left;
12 | }
13 |
14 | td {
15 | border-top: 1px solid #dedede;
16 | }
17 |
18 | // Force the width of numberic inputs so they don't take too much space when we know we rarely need more than ~5 digits
19 | .FlamarktDecimalInput {
20 | max-width: 10em;
21 | }
22 |
23 | // Reduce space used by form groups used inside of the table
24 | .Form-group, label {
25 | margin-bottom: 5px;
26 | }
27 | }
28 |
29 | .OrderLineEdit-Info {
30 | max-width: 20em;
31 | }
32 |
33 | .OrderLineEdit-Group, .OrderLineEdit-Type {
34 | .Select {
35 | // For some reason the Select is not reserving the space it really needs
36 | margin-right: 10px;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/migrations/20210326_000100_create_carts_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->create('flamarkt_carts', function (Blueprint $table) {
9 | $table->increments('id');
10 | $table->string('uid')->unique();
11 | $table->unsignedInteger('user_id')->nullable();
12 | $table->unsignedInteger('order_id')->nullable(); // No foreign constraint on purpose, it will be used to know if the cart has been ordered
13 | $table->unsignedInteger('product_count')->nullable();
14 | $table->unsignedInteger('price_total')->nullable();
15 | $table->timestamps();
16 |
17 | $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
18 | });
19 | },
20 | 'down' => function (Builder $schema) {
21 | $schema->dropIfExists('flamarkt_carts');
22 | },
23 | ];
24 |
--------------------------------------------------------------------------------
/src/Payment/PaymentValidator.php:
--------------------------------------------------------------------------------
1 | payment;
15 | }
16 |
17 | public function setPayment(Payment $payment)
18 | {
19 | $this->payment = $payment;
20 | }
21 |
22 | protected $rules = [
23 | 'method' => [
24 | 'nullable',
25 | 'string',
26 | 'max:255',
27 | ],
28 | 'identifier' => [
29 | 'nullable',
30 | 'string',
31 | 'max:255',
32 | ],
33 | 'amount' => [
34 | 'integer',
35 | 'min:-2147483648', // MySQL signed INT range
36 | 'max:2147483647',
37 | ],
38 | ];
39 | }
40 |
--------------------------------------------------------------------------------
/src/Api/Controller/CartUpdateController.php:
--------------------------------------------------------------------------------
1 | repository->findUidOrFail(Arr::get($request->getQueryParams(), 'id'), $actor);
28 |
29 | return $this->repository->update($cart, $actor, (array)Arr::get($request->getParsedBody(), 'data'));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/pages/ProductShowPage.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractShowPage from 'flamarkt/backoffice/common/pages/AbstractShowPage';
2 | import Product from '../../common/models/Product';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | export default class ProductShowPage extends AbstractShowPage {
5 | product: Product | null;
6 | saving: boolean;
7 | dirty: boolean;
8 | title: string;
9 | description: string;
10 | price: number;
11 | availabilityDriver: string | null;
12 | priceDriver: string | null;
13 | newRecord(): import("flarum/common/Model").default;
14 | findType(): string;
15 | show(product: Product): void;
16 | view(): any;
17 | fields(): ItemList;
18 | availabilityDriverOptions(): any;
19 | priceDriverOptions(): any;
20 | data(): {
21 | title: string;
22 | description: string;
23 | price: number;
24 | availabilityDriver: string | null;
25 | priceDriver: string | null;
26 | };
27 | onsubmit(event: Event): void;
28 | }
29 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/layouts/ProductIndexLayout.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Children } from 'mithril';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import AbstractShopLayout, { AbstractShopLayoutAttrs } from './AbstractShopLayout';
5 | import ProductListState from '../../common/states/ProductListState';
6 | export interface ProductIndexLayoutAttrs extends AbstractShopLayoutAttrs {
7 | state: ProductListState;
8 | }
9 | export default class ProductIndexLayout extends AbstractShopLayout {
10 | className(): string;
11 | title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
12 | currentPageHref(): string;
13 | contentTitle(): any;
14 | /**
15 | * Whether to show the "product disabled" information instead of the product list
16 | */
17 | showBrowsingDisabled(): boolean;
18 | content(): Children;
19 | bottomControls(): Children;
20 | filters(): ItemList;
21 | }
22 |
--------------------------------------------------------------------------------
/src/Api/Serializer/PaymentSerializer.php:
--------------------------------------------------------------------------------
1 | $payment->method,
22 | 'amount' => $payment->amount,
23 | 'createdAt' => $this->formatDate($payment->created_at),
24 | 'isHidden' => true, // TODO: soft-delete not yet implemented, but this attribute is necessary for the permanent delete component in the backend
25 | ];
26 |
27 | if ($this->actor->can('backoffice')) {
28 | $attributes['identifier'] = $payment->identifier;
29 | }
30 |
31 | return $attributes;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/js/dist-typings/common/components/DecimalInput.d.ts:
--------------------------------------------------------------------------------
1 | import Component, { ComponentAttrs } from 'flarum/common/Component';
2 | import Product from '../models/Product';
3 | export interface DecimalInputAttrs extends ComponentAttrs {
4 | value: number;
5 | onchange: (value: number) => void;
6 | disabled?: boolean;
7 | readonly?: boolean;
8 | product?: Product;
9 | unit?: string;
10 | min?: number;
11 | max?: number;
12 | step?: number;
13 | decimals?: number;
14 | className?: string;
15 | }
16 | export default class DecimalInput extends Component {
17 | hasFocus: boolean;
18 | decimals(): number;
19 | min(): number | undefined;
20 | max(): number | undefined;
21 | step(): number | undefined;
22 | disabled(): boolean;
23 | readonly(): boolean;
24 | className(): any[];
25 | fromIntegerValue(value: number): number;
26 | toIntegerValue(value: number): number;
27 | inputAttrs(): any;
28 | unitLabel(): string;
29 | view(): any;
30 | }
31 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/layouts/AbstractShopLayout.d.ts:
--------------------------------------------------------------------------------
1 | import { Children, Vnode } from 'mithril';
2 | import Component, { ComponentAttrs } from 'flarum/common/Component';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | export interface AbstractShopLayoutAttrs extends ComponentAttrs {
5 | }
6 | export default abstract class AbstractShopLayout extends Component {
7 | attrs: T;
8 | view(vnode: Vnode): any;
9 | sidebarItems(): ItemList;
10 | breadcrumbItems(): ItemList;
11 | className(): string;
12 | /**
13 | * Used as the breadcrumb current item as well as the title of the content
14 | * The title of the browser page needs to be set in the page itself and not the layout
15 | */
16 | title(): Children;
17 | /**
18 | * Used as a hint for the breadcrumb to not repeat an existing item if it happens to be the same as the active page
19 | */
20 | currentPageHref(): string | null;
21 | contentTitle(): any;
22 | abstract content(vnode: Vnode): Children;
23 | }
24 |
--------------------------------------------------------------------------------
/src/Order/Gambit/FullTextGambit.php:
--------------------------------------------------------------------------------
1 | isEnabled('clarkwinkelmann-scout')) {
16 | $search->getQuery()->where('id', $bit);
17 |
18 | return;
19 | }
20 |
21 | $builder = ScoutStatic::makeBuilder(Order::class, $bit);
22 |
23 | $ids = $builder->keys();
24 |
25 | $search->getQuery()->whereIn('id', $ids);
26 |
27 | $search->setDefaultSort(function ($query) use ($ids) {
28 | if (!count($ids)) {
29 | return;
30 | }
31 |
32 | $query->orderByRaw('FIELD(id' . str_repeat(', ?', count($ids)) . ')', $ids);
33 | });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/migrations/20210326_000500_create_order_payments_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->create('flamarkt_order_payments', function (Blueprint $table) {
9 | $table->increments('id');
10 | $table->string('uid')->unique();
11 | $table->unsignedInteger('order_id');
12 | $table->unsignedInteger('user_id')->nullable();
13 | $table->string('method')->nullable();
14 | $table->string('identifier')->nullable();
15 | $table->unsignedInteger('amount');
16 | $table->timestamps();
17 |
18 | $table->foreign('order_id')->references('id')->on('flamarkt_orders')->onDelete('cascade');
19 | $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
20 | });
21 | },
22 | 'down' => function (Builder $schema) {
23 | $schema->dropIfExists('flamarkt_order_payments');
24 | },
25 | ];
26 |
--------------------------------------------------------------------------------
/src/Api/Controller/PaymentUpdateController.php:
--------------------------------------------------------------------------------
1 | repository->findUidOrFail(Arr::get($request->getQueryParams(), 'id'), $actor);
28 |
29 | return $this->repository->update($payment, $actor, (array)Arr::get($request->getParsedBody(), 'data'));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Product/UserState.php:
--------------------------------------------------------------------------------
1 | belongsTo(Product::class);
27 | }
28 |
29 | public function user(): BelongsTo
30 | {
31 | return $this->belongsTo(User::class);
32 | }
33 |
34 | protected function setKeysForSaveQuery($query): Builder
35 | {
36 | $query->where('product_id', $this->product_id)
37 | ->where('user_id', $this->user_id);
38 |
39 | return $query;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Order/OrderServiceProvider.php:
--------------------------------------------------------------------------------
1 | container->instance('flamarkt.order.line.groups', [
13 | 'shipping',
14 | ]);
15 |
16 | $this->container->instance('flamarkt.order.line.types', [
17 | 'product',
18 | 'manual',
19 | ]);
20 |
21 | $this->container->instance('flamarkt.payment.callbacks', [
22 | 'partial' => [],
23 | 'remaining' => [],
24 | ]);
25 |
26 | $this->container->when(OrderBuilderFactory::class)
27 | ->needs('$paymentCallbacks')
28 | ->give(function () {
29 | $callbacks = $this->container->make('flamarkt.payment.callbacks');
30 |
31 | return array_merge(Arr::get($callbacks, 'partial'), Arr::get($callbacks, 'remaining'));
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/components/CartTableRow.d.ts:
--------------------------------------------------------------------------------
1 | import Component, { ComponentAttrs } from 'flarum/common/Component';
2 | import ItemList from 'flarum/common/utils/ItemList';
3 | import Product from '../../common/models/Product';
4 | import { InlineSubmitStatusResult } from './InlineSubmitStatus';
5 | interface CartTableRowAttrs extends ComponentAttrs {
6 | product: Product;
7 | }
8 | export default class CartTableRow extends Component {
9 | quantity: number;
10 | savedQuantity: number;
11 | quantitySaving: boolean;
12 | quantitySaveResult: InlineSubmitStatusResult;
13 | quantityChangeDebounce: number;
14 | oninit(vnode: any): void;
15 | view(): any;
16 | columns(): ItemList;
17 | inlineLoading(): boolean;
18 | inlineResult(): InlineSubmitStatusResult;
19 | quantityEditDisabled(): boolean;
20 | deleteDisabled(): boolean;
21 | deleteTooltipText(): string;
22 | submitQuantity(quantity: number): void;
23 | submitQuantityDone(product: Product): void;
24 | submitQuantityError(error: any): void;
25 | }
26 | export {};
27 |
--------------------------------------------------------------------------------
/src/Product/Gambit/FullTextGambit.php:
--------------------------------------------------------------------------------
1 | isEnabled('clarkwinkelmann-scout')) {
16 | $search->getQuery()->where('title', 'like', '%' . $bit . '%');
17 |
18 | return;
19 | }
20 |
21 | $builder = ScoutStatic::makeBuilder(Product::class, $bit);
22 |
23 | $ids = $builder->keys();
24 |
25 | $search->getQuery()->whereIn('id', $ids);
26 |
27 | $search->setDefaultSort(function ($query) use ($ids) {
28 | if (!count($ids)) {
29 | return;
30 | }
31 |
32 | $query->orderByRaw('FIELD(id' . str_repeat(', ?', count($ids)) . ')', $ids);
33 | });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Listener/SaveUser.php:
--------------------------------------------------------------------------------
1 | data, 'attributes');
16 |
17 | if (!empty($attributes['preferences'])) {
18 | // We know that if we reach this point, Flarum has already asserted permission in EditUserHandler
19 | foreach ($attributes['preferences'] as $k => $v) {
20 | // Prevent enabling order received web alert
21 | // If the default is false and we block here there shouldn't be any way to enable it
22 | if ($k === User::getNotificationPreferenceKey(OrderReceivedBlueprint::getType(), 'alert')) {
23 | throw new PermissionDeniedException();
24 | }
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/views/frontend/content/products.blade.php:
--------------------------------------------------------------------------------
1 | @inject('url', 'Flarum\Http\UrlGenerator')
2 |
3 |
26 |
--------------------------------------------------------------------------------
/src/Product/CartState.php:
--------------------------------------------------------------------------------
1 | belongsTo(Cart::class);
28 | }
29 |
30 | public function product(): BelongsTo
31 | {
32 | return $this->belongsTo(Product::class);
33 | }
34 |
35 | protected function setKeysForSaveQuery($query): Builder
36 | {
37 | $query->where('cart_id', $this->cart_id)
38 | ->where('product_id', $this->product_id);
39 |
40 | return $query;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Api/Serializer/OrderLineSerializer.php:
--------------------------------------------------------------------------------
1 | $line->number,
23 | 'group' => $line->group,
24 | 'type' => $line->type,
25 | 'label' => $line->label,
26 | 'comment' => $line->comment,
27 | 'quantity' => $line->quantity,
28 | 'priceUnit' => $line->price_unit,
29 | 'priceTotal' => $line->price_total,
30 | ];
31 | }
32 |
33 | public function product(OrderLine $line): ?Relationship
34 | {
35 | return $this->hasOne($line, BasicProductSerializer::class);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/resources/less/forum/OrderFacts.less:
--------------------------------------------------------------------------------
1 | .FlamarktOrderFacts {
2 | display: flex;
3 | flex-wrap: wrap;
4 | padding: 10px 0;
5 | }
6 |
7 | .FlamarktOrderFact {
8 | margin-right: 20px;
9 | width: 227px; // Enough space for 3 columns on "tablet" breakpoint and on "desktop" with sidebar
10 | max-width: 100%;
11 |
12 | @media @desktop-hd {
13 | width: 250px; // Enough space for 3 columns with sidebar visible
14 | }
15 |
16 | &:last-child {
17 | margin-right: 0;
18 | }
19 |
20 | dl {
21 | margin: 0;
22 | }
23 |
24 | dt {
25 | font-weight: bold;
26 | }
27 |
28 | dd {
29 | margin: 5px 0 0 0;
30 | }
31 | }
32 |
33 | .FlamarktOrderFact--payment {
34 | display: table;
35 |
36 | dd {
37 | display: table-row;
38 |
39 | > span {
40 | display: table-cell;
41 | padding: 2px 6px 2px 0;
42 |
43 | &.date {
44 | color: @muted-color;
45 | }
46 |
47 | &.amount {
48 | text-align: right;
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Product/Gambit/TypeGambit.php:
--------------------------------------------------------------------------------
1 | constrain($search->getQuery(), $matches[1], $negate);
21 | }
22 |
23 | public function getFilterKey(): string
24 | {
25 | return 'type';
26 | }
27 |
28 | public function filter(FilterState $filterState, string $filterValue, bool $negate)
29 | {
30 | $this->constrain($filterState->getQuery(), $filterValue, $negate);
31 | }
32 |
33 | protected function constrain(Builder $query, $value, $negate)
34 | {
35 | $query->where('flamarkt_products.type', $negate ? '!=' : '=', $value);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021-2022 Clark Winkelmann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Listener/UpdateUserOrderMeta.php:
--------------------------------------------------------------------------------
1 | listen([
14 | Event\Created::class,
15 | Event\Deleted::class,
16 | Event\UserChanged::class,
17 | ], [$this, 'handle']);
18 | }
19 |
20 | /**
21 | * @param Event\Created|Event\Deleted|Event\UserChanged $event
22 | */
23 | public function handle($event): void
24 | {
25 | if ($event instanceof Event\UserChanged && $event->oldUser) {
26 | $this->updateUser($event->oldUser);
27 | }
28 |
29 | if ($event->order->user) {
30 | $this->updateUser($event->order->user);
31 | }
32 | }
33 |
34 | protected function updateUser(User $user): void
35 | {
36 | // We include soft-deleted orders
37 | $user->flamarkt_order_count = $user->orders()->count();
38 | $user->save();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Api/Controller/OrderUpdateController.php:
--------------------------------------------------------------------------------
1 | repository->findUidOrFail(Arr::get($request->getQueryParams(), 'id'), $actor);
34 |
35 | return $this->repository->update($order, $actor, (array)Arr::get($request->getParsedBody(), 'data'));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Api/Controller/PaymentStoreController.php:
--------------------------------------------------------------------------------
1 | orderRepository->findUidOrFail(Arr::get($request->getQueryParams(), 'id'), $actor);
30 |
31 | return $this->paymentRepository->store($order, $actor, (array)Arr::get($request->getParsedBody(), 'data'));
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/pages/OrderShowPage.d.ts:
--------------------------------------------------------------------------------
1 | import { Vnode } from 'mithril';
2 | import User from 'flarum/common/models/User';
3 | import ItemList from 'flarum/common/utils/ItemList';
4 | import AbstractShowPage from 'flamarkt/backoffice/common/pages/AbstractShowPage';
5 | import Order from '../../common/models/Order';
6 | import OrderLine from '../../common/models/OrderLine';
7 | import OrderLineEditState from '../states/OrderLineEditState';
8 | export default class OrderShowPage extends AbstractShowPage {
9 | order: Order | null;
10 | user: User | null;
11 | lines: OrderLineEditState[];
12 | saving: boolean;
13 | dirty: boolean;
14 | newLine: OrderLineEditState;
15 | oninit(vnode: Vnode): void;
16 | initNewLine(): void;
17 | initLineState(line: OrderLine): OrderLineEditState;
18 | newRecord(): import("flarum/common/Model").default;
19 | findType(): string;
20 | show(order: Order): void;
21 | view(): any;
22 | fields(): ItemList;
23 | tableHead(): ItemList;
24 | data(): {
25 | relationships: {
26 | lines: any[];
27 | user: User | null;
28 | };
29 | };
30 | onsubmit(event: Event): void;
31 | }
32 |
--------------------------------------------------------------------------------
/resources/less/forum.less:
--------------------------------------------------------------------------------
1 | .Breadcrumb {
2 | list-style: none;
3 | margin: 0 0 10px;
4 | padding: 6px 10px;
5 | background: #f6f6f6;
6 | border-radius: @border-radius;
7 |
8 | li {
9 | display: inline-block;
10 |
11 | &:not(:first-child)::before {
12 | display: inline-block;
13 | content: '>';
14 | color: @muted-color;
15 | padding: 0 0.3em;
16 | }
17 | }
18 | }
19 |
20 | // We have a .container class on some pages but it's not really used yet
21 | // But is impacted by Flarum global style since it's intended for full width
22 | // We just cancel the effects for now
23 | .sideNavOffset .container {
24 | width: auto;
25 | margin: 0;
26 | }
27 |
28 | .ProductShowSections {
29 | display: flex;
30 | flex-wrap: wrap;
31 | }
32 |
33 | .ProductShowSection {
34 | width: 100%;
35 | }
36 |
37 | @media @tablet-up {
38 | .ProductShowSection--gallery,
39 | .ProductShowSection--gallery + .ProductShowSection--price {
40 | width: 50%;
41 | }
42 | }
43 |
44 | @import 'common/DecimalInput';
45 | @import 'forum/CartDropdown';
46 | @import 'forum/CartPage';
47 | @import 'forum/InlineSubmitStatus';
48 | @import 'forum/OrderFacts';
49 | @import 'forum/ProductList';
50 |
--------------------------------------------------------------------------------
/src/Api/Serializer/CartSerializer.php:
--------------------------------------------------------------------------------
1 | $cart->product_count,
30 | 'priceTotal' => $cart->price_total,
31 | 'amountDueAfterPartial' => $cart->amount_due_after_partial,
32 | 'canAddProducts' => $this->actor->can('addProducts', $cart),
33 | 'canCheckout' => $this->actor->can('checkout', $cart),
34 | 'isLocked' => $this->lock->isContentLocked($cart) || $this->lock->isSubmitLocked($cart),
35 | ];
36 | }
37 |
38 | public function products(Cart $cart): ?Relationship
39 | {
40 | return $this->hasMany($cart, ProductSerializer::class);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/migrations/20210326_000400_create_order_lines_table.php:
--------------------------------------------------------------------------------
1 | function (Builder $schema) {
8 | $schema->create('flamarkt_order_lines', function (Blueprint $table) {
9 | $table->increments('id');
10 | $table->string('uid')->unique();
11 | $table->unsignedInteger('order_id');
12 | $table->unsignedInteger('product_id')->nullable();
13 | $table->unsignedInteger('number');
14 | $table->string('group')->nullable();
15 | $table->string('type')->nullable();
16 | $table->string('label')->nullable();
17 | $table->text('comment')->nullable();
18 | $table->unsignedInteger('price_unit')->nullable();
19 | $table->unsignedInteger('quantity')->nullable();
20 | $table->unsignedInteger('price_total');
21 |
22 | $table->foreign('order_id')->references('id')->on('flamarkt_orders')->onDelete('cascade');
23 | $table->foreign('product_id')->references('id')->on('flamarkt_products')->onDelete('set null');
24 | });
25 | },
26 | 'down' => function (Builder $schema) {
27 | $schema->dropIfExists('flamarkt_order_lines');
28 | },
29 | ];
30 |
--------------------------------------------------------------------------------
/src/Cart/Access/CartPolicy.php:
--------------------------------------------------------------------------------
1 | deny();
17 | }
18 |
19 | if (resolve(CartLock::class)->isContentLocked($cart)) {
20 | return $this->deny();
21 | }
22 |
23 | if ($actor->hasPermission('backoffice')) {
24 | return $this->allow();
25 | }
26 |
27 | // TODO: match guest cart with correct guest session
28 | return $actor->id === $cart->user_id && $actor->hasPermission('flamarkt.cart');
29 | }
30 |
31 | public function checkout(User $actor, Cart $cart)
32 | {
33 | if ($cart instanceof GuestCart) {
34 | return $this->deny();
35 | }
36 |
37 | if ($actor->hasPermission('backoffice')) {
38 | return $this->allow();
39 | }
40 |
41 | // TODO: match guest cart with correct guest session
42 | return $actor->id === $cart->user_id && $actor->hasPermission('flamarkt.checkout');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Api/Controller/CartSessionController.php:
--------------------------------------------------------------------------------
1 | getAttribute('cart');
28 |
29 | // Right now guests might not have a cart, so we'll throw an error
30 | // In the future it would be ideal for a cart to always exist
31 | if (!$cart) {
32 | return new GuestCart();
33 | }
34 |
35 | Product::setStateUser(RequestUtil::getActor($request));
36 | Product::setStateCart($cart);
37 |
38 | $cart->load([
39 | 'products.cartState',
40 | 'products.userState',
41 | ]);
42 |
43 | return $cart;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Api/Controller/OrderShowController.php:
--------------------------------------------------------------------------------
1 | getQueryParams(), 'id');
35 | $actor = RequestUtil::getActor($request);
36 |
37 | if (Arr::get($request->getQueryParams(), 'bySlug')) {
38 | return $this->slugManager->forResource(Order::class)->fromSlug($id, $actor);
39 | } else {
40 | return $this->repository->findUidOrFail($id, $actor);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Api/Controller/ProductShowController.php:
--------------------------------------------------------------------------------
1 | getQueryParams(), 'id');
29 | $actor = RequestUtil::getActor($request);
30 |
31 | if (Arr::get($request->getQueryParams(), 'bySlug')) {
32 | $product = $this->slugManager->forResource(Product::class)->fromSlug($id, $actor);
33 | } else {
34 | $product = $this->repository->findUidOrFail($id, $actor);
35 | }
36 |
37 | Product::setStateCart($request->getAttribute('cart'));
38 |
39 | return $product;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Api/Controller/ProductUpdateController.php:
--------------------------------------------------------------------------------
1 | getAttribute('cart');
32 |
33 | $product = $this->repository->findUidOrFail(Arr::get($request->getQueryParams(), 'id'), $actor);
34 |
35 | $product = $this->repository->update($product, $actor, (array)Arr::get($request->getParsedBody(), 'data'), $cart);
36 |
37 | Product::setStateCart($cart);
38 |
39 | if (!$product->cart) {
40 | $this->removeInclude('cart');
41 | }
42 |
43 | return $product;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Order/OrderLine.php:
--------------------------------------------------------------------------------
1 | 'int',
34 | 'price_unit' => 'int',
35 | 'quantity' => 'int',
36 | 'price_total' => 'int',
37 | ];
38 |
39 | public function order(): Relations\BelongsTo
40 | {
41 | return $this->belongsTo(Order::class);
42 | }
43 |
44 | public function product(): Relations\BelongsTo
45 | {
46 | return $this->belongsTo(Product::class);
47 | }
48 |
49 | public function updateTotal(): self
50 | {
51 | $this->price_total = $this->quantity * $this->price_unit;
52 |
53 | return $this;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/js/dist-typings/forum/layouts/ProductShowLayout.d.ts:
--------------------------------------------------------------------------------
1 | import { Children } from 'mithril';
2 | import AbstractShopLayout, { AbstractShopLayoutAttrs } from './AbstractShopLayout';
3 | import Product from '../../common/models/Product';
4 | import ItemList from 'flarum/common/utils/ItemList';
5 | export interface ProductShowLayoutAttrs extends AbstractShopLayoutAttrs {
6 | product?: Product;
7 | }
8 | export default class ProductShowLayout extends AbstractShopLayout {
9 | breadcrumbItems(): ItemList;
10 | className(): string;
11 | /**
12 | * The actively displayed product, which can be different from this.attrs.product
13 | * This allows extensions like flamarkt/variants to change the displayed product without changing the page
14 | */
15 | product(): Product | undefined;
16 | title(): string;
17 | contentTitle(): null;
18 | /**
19 | * Whether to show the "browsing disabled" information message if the product cannot be loaded
20 | */
21 | showBrowsingDisabled(): boolean;
22 | content(): any;
23 | loadingContent(): Children;
24 | loadedContent(product: Product): Children;
25 | sections(product: Product): ItemList;
26 | gallerySection(product: Product): ItemList;
27 | priceSection(product: Product): ItemList;
28 | showCartControls(product: Product): boolean;
29 | descriptionSection(product: Product): ItemList;
30 | }
31 |
--------------------------------------------------------------------------------
/src/Database/HasUid.php:
--------------------------------------------------------------------------------
1 | toString();
22 | }
23 |
24 | protected static function bootHasUid(): void
25 | {
26 | static::creating(function (self $model) {
27 | // If already generated by getter or manually set by an extension, keep existing value
28 | if ($model->uid) {
29 | return;
30 | }
31 |
32 | $model->uid = $model->generateUidValue();
33 | });
34 | }
35 |
36 | public function getUidAttribute($value)
37 | {
38 | if ($this->exists || $value) {
39 | return $value;
40 | }
41 |
42 | $this->attributes['uid'] = $this->generateUidValue();
43 |
44 | return $this->attributes['uid'];
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Payment/Payment.php:
--------------------------------------------------------------------------------
1 | 'int',
39 | 'created_at' => 'datetime',
40 | 'updated_at' => 'datetime',
41 | ];
42 |
43 | public function order(): Relations\BelongsTo
44 | {
45 | return $this->belongsTo(Order::class);
46 | }
47 |
48 | public function product(): Relations\BelongsTo
49 | {
50 | return $this->belongsTo(Product::class);
51 | }
52 |
53 | public function user(): Relations\BelongsTo
54 | {
55 | return $this->belongsTo(User::class);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Api/Serializer/OrderSerializer.php:
--------------------------------------------------------------------------------
1 | $order->price_total,
19 | 'paidAmount' => $order->paid_amount,
20 | 'productCount' => $order->product_count,
21 | 'createdAt' => $this->formatDate($order->created_at),
22 | ];
23 |
24 | if ($order->hidden_at) {
25 | $attributes['isHidden'] = true;
26 |
27 | if ($this->actor->can('backoffice')) {
28 | $attributes['hiddenAt'] = $this->formatDate($order->hidden_at);
29 | }
30 | }
31 |
32 | return $attributes;
33 | }
34 |
35 | public function user(Order $order): ?Relationship
36 | {
37 | return $this->hasOne($order, BasicUserSerializer::class);
38 | }
39 |
40 | public function lines(Order $order): ?Relationship
41 | {
42 | return $this->hasMany($order, OrderLineSerializer::class);
43 | }
44 |
45 | public function payments(Order $order): ?Relationship
46 | {
47 | return $this->hasMany($order, PaymentSerializer::class);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Notification/MailConfig.php:
--------------------------------------------------------------------------------
1 | get('theme_primary_color');
22 | }
23 |
24 | public static $brandCallback;
25 |
26 | public static function brand(): string
27 | {
28 | if (self::$brandCallback) {
29 | return call_user_func(self::$brandCallback);
30 | }
31 |
32 | $url = resolve(Config::class)->url();
33 | $title = resolve(SettingsRepositoryInterface::class)->get('forum_title');
34 |
35 | return '' . e($title) . '';
36 | }
37 |
38 | public static $cssCallbacks = [];
39 |
40 | public static function css(BlueprintInterface $blueprint = null): string
41 | {
42 | $allCss = [];
43 |
44 | foreach (self::$cssCallbacks as $callback) {
45 | $css = call_user_func($callback, $blueprint);
46 |
47 | if ($css) {
48 | $allCss[] = $css;
49 | }
50 | }
51 |
52 | return implode("\n\n", $allCss);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Product/AbstractManager.php:
--------------------------------------------------------------------------------
1 | driver($product, $actor, $request);
23 |
24 | $driverCallback = Arr::get($this->drivers, $driverName);
25 |
26 | if (!is_callable($driverCallback)) {
27 | throw new \Exception("Invalid callback for driver $driverName");
28 | }
29 |
30 | $results = [
31 | $this->sanitize($driverCallback($product, $actor, $request)),
32 | ];
33 |
34 | $filters = array_merge(
35 | Arr::get($this->filters, $driverName) ?? [],
36 | Arr::get($this->filters, '*') ?? []
37 | );
38 |
39 | foreach ($filters as $callback) {
40 | $results[] = $this->sanitize($callback($product, $actor, $request));
41 | }
42 |
43 | return $results;
44 | }
45 |
46 | abstract function driver(Product $product, User $actor, ServerRequestInterface $request = null): string;
47 |
48 | protected function sanitize($result)
49 | {
50 | return $result;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Api/Controller/OrderStoreController.php:
--------------------------------------------------------------------------------
1 | getParsedBody(), 'data');
37 |
38 | $cartId = Arr::get($data, 'relationships.cart.data.id');
39 |
40 | if ($cartId) {
41 | $cart = $this->cartRepository->findUidOrFail($cartId, $actor);
42 |
43 | return $this->orderBuilder->build($actor, $cart, $data, $request);
44 | }
45 |
46 | // If no cart ID was passed, continue to manual admin order creation
47 | return $this->orderRepository->store($actor, $data);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/js/shims.d.ts:
--------------------------------------------------------------------------------
1 | import ForumApplication from 'flarum/forum/ForumApplication';
2 | import AdminApplication from 'flarum/admin/AdminApplication';
3 | import User from 'flarum/common/models/User';
4 | import CartState from './src/forum/states/CartState';
5 | import Cart from './src/common/models/Cart';
6 | import Order from './src/common/models/Order';
7 | import Product from './src/common/models/Product';
8 |
9 | export interface AdditionalApplication {
10 | cart: CartState
11 | route: {
12 | product(product: Product): string,
13 | user(user: User): string,
14 | order(order: Order): string,
15 | }
16 | }
17 |
18 | declare module 'flarum/forum/ForumApplication' {
19 | export default interface ForumApplication {
20 | cart: CartState
21 | route: {
22 | product(product: Product): string,
23 | user(user: User): string,
24 | order(order: Order): string,
25 | }
26 | }
27 | }
28 |
29 | declare global {
30 | const app: ForumApplication & AdminApplication & AdditionalApplication;
31 | }
32 |
33 | declare module 'flarum/common/helpers/username' {
34 | // Allow passing null or undefined
35 | export default function username(user: User | null | undefined | false): any;
36 | }
37 |
38 | declare module 'flarum/common/models/User' {
39 | export default interface User {
40 | slug(): string
41 |
42 | username(value?: string): string
43 |
44 | email(value?: string): string
45 |
46 | flamarktOrderCount(value?: number): number
47 | }
48 | }
49 |
50 | declare module 'flarum/common/models/Forum' {
51 | export default interface Forum {
52 | cart(): Cart | false
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Forum/Content/Product.php:
--------------------------------------------------------------------------------
1 | getQueryParams();
26 | $id = Arr::get($queryParams, 'id');
27 |
28 | $apiDocument = $this->getApiDocument($request, $id);
29 | $product = $apiDocument->data->attributes;
30 |
31 | $document->title = $product->title;
32 | $document->canonicalUrl = $this->url->to('forum')->route('flamarkt.products.show', ['id' => $product->slug]);
33 | $document->content = $this->view->make('flamarkt::frontend.content.product', compact('apiDocument'));
34 | $document->payload['apiDocument'] = $apiDocument;
35 | }
36 |
37 | protected function getApiDocument(ServerRequestInterface $request, string $id)
38 | {
39 | $response = $this->api->withParentRequest($request)->withQueryParams(['bySlug' => true])->get("/flamarkt/products/$id");
40 | $statusCode = $response->getStatusCode();
41 |
42 | if ($statusCode === 404) {
43 | throw new ModelNotFoundException;
44 | }
45 |
46 | return json_decode($response->getBody());
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/js/dist-typings/common/compat.d.ts:
--------------------------------------------------------------------------------
1 | export const common: {
2 | 'components/DecimalInput': typeof DecimalInput;
3 | 'components/DecimalLabel': typeof DecimalLabel;
4 | 'components/OrderSortDropdown': typeof OrderSortDropdown;
5 | 'components/PriceInput': typeof PriceInput;
6 | 'components/PriceLabel': typeof PriceLabel;
7 | 'components/ProductSortDropdown': typeof ProductSortDropdown;
8 | 'components/QuantityInput': typeof QuantityInput;
9 | 'components/QuantityLabel': typeof QuantityLabel;
10 | 'helpers/formatPrice': typeof formatPrice;
11 | 'models/Cart': typeof Cart;
12 | 'models/Order': typeof Order;
13 | 'models/OrderLine': typeof OrderLine;
14 | 'models/Payment': typeof Payment;
15 | 'models/Product': typeof Product;
16 | 'states/OrderListState': typeof OrderListState;
17 | 'states/ProductListState': typeof ProductListState;
18 | };
19 | import DecimalInput from "./components/DecimalInput";
20 | import DecimalLabel from "./components/DecimalLabel";
21 | import OrderSortDropdown from "./components/OrderSortDropdown";
22 | import PriceInput from "./components/PriceInput";
23 | import PriceLabel from "./components/PriceLabel";
24 | import ProductSortDropdown from "./components/ProductSortDropdown";
25 | import QuantityInput from "./components/QuantityInput";
26 | import QuantityLabel from "./components/QuantityLabel";
27 | import formatPrice from "./helpers/formatPrice";
28 | import Cart from "./models/Cart";
29 | import Order from "./models/Order";
30 | import OrderLine from "./models/OrderLine";
31 | import Payment from "./models/Payment";
32 | import Product from "./models/Product";
33 | import OrderListState from "./states/OrderListState";
34 | import ProductListState from "./states/ProductListState";
35 |
--------------------------------------------------------------------------------
/src/Forum/Content/Order.php:
--------------------------------------------------------------------------------
1 | getQueryParams();
26 | $id = Arr::get($queryParams, 'id');
27 |
28 | $apiDocument = $this->getApiDocument($request, $id);
29 | $order = $apiDocument->data->attributes;
30 |
31 | $document->title = $this->translator->trans('flamarkt-core.forum.order.headingTitle', [
32 | '{number}' => $order->number,
33 | ]);
34 | $document->canonicalUrl = $this->url->to('forum')->route('flamarkt.orders.show', ['id' => $order->slug]);
35 | $document->payload['apiDocument'] = $apiDocument;
36 | }
37 |
38 | protected function getApiDocument(ServerRequestInterface $request, string $id)
39 | {
40 | $response = $this->api->withParentRequest($request)->withQueryParams(['bySlug' => true])->get("/flamarkt/orders/$id");
41 | $statusCode = $response->getStatusCode();
42 |
43 | if ($statusCode === 404) {
44 | throw new ModelNotFoundException;
45 | }
46 |
47 | return json_decode($response->getBody());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Notification/AbstractOrderUpdateBlueprint.php:
--------------------------------------------------------------------------------
1 | order;
24 | }
25 |
26 | public function getFromUser(): ?User
27 | {
28 | return null;
29 | }
30 |
31 | public function getData()
32 | {
33 | }
34 |
35 | public static function getSubjectModel(): string
36 | {
37 | return Order::class;
38 | }
39 |
40 | public function getEmailView(): array
41 | {
42 | return ['html' => 'flamarkt::email.orderUpdated'];
43 | }
44 |
45 | public function getEmailSubject(TranslatorInterface $translator): string
46 | {
47 | return $translator->trans('flamarkt-core.email.orderUpdated.subject', [
48 | '{number}' => $this->order->id,
49 | ]);
50 | }
51 |
52 | public function getEmailMessage(TranslatorInterface $translator): string
53 | {
54 | return '' . htmlspecialchars($translator->trans('flamarkt-core.email.orderUpdated.message')) . '
';
55 | }
56 |
57 | public function getEmailComponent(): ComponentInterface
58 | {
59 | return new OrderComponent($this->order, $this->getType());
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Order/Gambit/UserGambit.php:
--------------------------------------------------------------------------------
1 | slugDriver = $slugManager->forResource(User::class);
22 | }
23 |
24 | protected function getGambitPattern(): string
25 | {
26 | return 'user:(.+)';
27 | }
28 |
29 | protected function conditions(SearchState $search, array $matches, $negate)
30 | {
31 | $this->constrain($search->getQuery(), $matches[1], $negate, $search->getActor());
32 | }
33 |
34 | public function getFilterKey(): string
35 | {
36 | return 'user';
37 | }
38 |
39 | public function filter(FilterState $filterState, string $filterValue, bool $negate)
40 | {
41 | $this->constrain($filterState->getQuery(), $filterValue, $negate, $filterState->getActor());
42 | }
43 |
44 | protected function constrain(Builder $query, $rawSlugs, $negate, User $actor)
45 | {
46 | $slugs = trim($rawSlugs, '"');
47 | $slugs = explode(',', $slugs);
48 |
49 | $ids = [];
50 | foreach ($slugs as $slug) {
51 | $ids[] = $this->slugDriver->fromSlug($slug, $actor)->id;
52 | }
53 |
54 | $query->whereIn('flamarkt_orders.user_id', $ids, 'and', $negate);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Extend/Price.php:
--------------------------------------------------------------------------------
1 | drivers[$name] = $className;
18 |
19 | return $this;
20 | }
21 |
22 | public function driverFilter(string $name, $callback): self
23 | {
24 | $this->filters[$name][] = $callback;
25 |
26 | return $this;
27 | }
28 |
29 | public function globalFilter($callback): self
30 | {
31 | $this->filters['*'][] = $callback;
32 |
33 | return $this;
34 | }
35 |
36 | public function extend(Container $container, Extension $extension = null)
37 | {
38 | if (count($this->drivers)) {
39 | $container->extend('flamarkt.price_drivers', function (array $drivers) use ($container): array {
40 | foreach ($this->drivers as $name => $driver) {
41 | $drivers[$name] = ContainerUtil::wrapCallback($driver, $container);
42 | }
43 |
44 | return $drivers;
45 | });
46 | }
47 |
48 | if (count($this->filters)) {
49 | $container->extend('flamarkt.price_driver_filters', function (array $filters) use ($container): array {
50 | foreach ($this->filters as $key => $keyFilters) {
51 | foreach ($keyFilters as $filter) {
52 | $filters[$key][] = ContainerUtil::wrapCallback($filter, $container);
53 | }
54 | }
55 |
56 | return $filters;
57 | });
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Extend/Availability.php:
--------------------------------------------------------------------------------
1 | drivers[$name] = $className;
18 |
19 | return $this;
20 | }
21 |
22 | public function driverFilter(string $name, $callback): self
23 | {
24 | $this->filters[$name][] = $callback;
25 |
26 | return $this;
27 | }
28 |
29 | public function globalFilter($callback): self
30 | {
31 | $this->filters['*'][] = $callback;
32 |
33 | return $this;
34 | }
35 |
36 | public function extend(Container $container, Extension $extension = null)
37 | {
38 | if (count($this->drivers)) {
39 | $container->extend('flamarkt.availability_drivers', function (array $drivers) use ($container): array {
40 | foreach ($this->drivers as $name => $driver) {
41 | $drivers[$name] = ContainerUtil::wrapCallback($driver, $container);
42 | }
43 |
44 | return $drivers;
45 | });
46 | }
47 |
48 | if (count($this->filters)) {
49 | $container->extend('flamarkt.availability_driver_filters', function (array $filters) use ($container): array {
50 | foreach ($this->filters as $key => $keyFilters) {
51 | foreach ($keyFilters as $filter) {
52 | $filters[$key][] = ContainerUtil::wrapCallback($filter, $container);
53 | }
54 | }
55 |
56 | return $filters;
57 | });
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Cart/CartMiddleware.php:
--------------------------------------------------------------------------------
1 | getAttribute('session');
26 |
27 | // Most of the time Flarum will run with a session, but requests to the mithril2html frontend will skip it
28 | // In that case we just ignore that part just like we do for guests
29 | if (!$session) {
30 | return $handler->handle($request);
31 | }
32 |
33 | $actor = RequestUtil::getActor($request);
34 |
35 | if ($actor->isGuest() || !$actor->hasPermission('flamarkt.cart')) {
36 | // TODO: allow guests. Currently disabled because there's no way to match which cart belongs to which guest
37 | return $handler->handle($request->withAttribute('cart', new GuestCart()));
38 | }
39 |
40 | $cartUid = $session->get('cart_uid');
41 |
42 | if ($cartUid) {
43 | $cart = $this->repository->findUid($cartUid, $actor);
44 |
45 | if ($cart && !$cart->alreadySubmitted) {
46 | return $handler->handle($request->withAttribute('cart', $cart));
47 | }
48 | }
49 |
50 | $cart = $this->repository->store($actor);
51 |
52 | $session->put('cart_uid', $cart->uid);
53 |
54 | return $handler->handle($request->withAttribute('cart', $cart));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Forum/Content/Products.php:
--------------------------------------------------------------------------------
1 | getQueryParams();
29 |
30 | $sort = Arr::pull($queryParams, 'sort');
31 | $q = Arr::pull($queryParams, 'q');
32 | $page = max(1, intval(Arr::pull($queryParams, 'page')));
33 |
34 | $params = [
35 | 'sort' => $sort,
36 | 'filter' => [],
37 | 'page' => ['offset' => ($page - 1) * 24, 'limit' => 24],
38 | ];
39 |
40 | if ($q) {
41 | $params['filter']['q'] = $q;
42 | }
43 |
44 | $apiDocument = $this->getApiDocument($request, $params);
45 |
46 | $document->title = $this->translator->trans('flamarkt-core.forum.products.headingTitle');
47 | $document->content = $this->view->make('flamarkt::frontend.content.products', compact('apiDocument', 'page'));
48 | $document->payload['apiDocument'] = $apiDocument;
49 | }
50 |
51 | protected function getApiDocument(ServerRequestInterface $request, array $params)
52 | {
53 | return json_decode($this->api->withParentRequest($request)->withQueryParams($params)->get('/flamarkt/products')->getBody());
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Product/AvailabilityManager.php:
--------------------------------------------------------------------------------
1 | false,
17 | self::FORCE_AVAILABLE => true,
18 | self::UNAVAILABLE => false,
19 | self::AVAILABLE => true,
20 | ];
21 |
22 | public function driver(Product $product, User $actor, ServerRequestInterface $request = null): string
23 | {
24 | return $product->availability_driver ?? $this->settings->get('flamarkt.defaultAvailabilityDriver') ?: 'never';
25 | }
26 |
27 | protected function sanitize($result)
28 | {
29 | if ($result === true) {
30 | return self::AVAILABLE;
31 | }
32 |
33 | if ($result === false) {
34 | return self::UNAVAILABLE;
35 | }
36 |
37 | return $result;
38 | }
39 |
40 | public function availability(Product $product, User $actor, ServerRequestInterface $request = null): bool
41 | {
42 | $results = $this->process($product, $actor, $request);
43 |
44 | foreach (static::EVALUATION_CRITERIA_PRIORITY as $criteria => $decision) {
45 | if (in_array($criteria, $results, true)) {
46 | return $decision;
47 | }
48 | }
49 |
50 | return false;
51 | }
52 |
53 | public function canOrder(Product $product, User $actor, ServerRequestInterface $request = null): bool
54 | {
55 | if ($actor->can('orderAlways', $product)) {
56 | return true;
57 | }
58 |
59 | return $actor->can('orderWhenAvailable', $product) && $this->availability($product, $actor, $request);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Product/ProductServiceProvider.php:
--------------------------------------------------------------------------------
1 | container->instance('flamarkt.availability_drivers', [
14 | 'always' => new AlwaysAvailable,
15 | 'never' => new NeverAvailable,
16 | ]);
17 | $this->container->instance('flamarkt.availability_driver_filters', []);
18 |
19 | $this->container->singleton(AvailabilityManager::class);
20 | $this->container->when(AvailabilityManager::class)
21 | ->needs('$drivers')
22 | ->give(function (): array {
23 | return $this->container->make('flamarkt.availability_drivers');
24 | });
25 | $this->container->when(AvailabilityManager::class)
26 | ->needs('$filters')
27 | ->give(function (): array {
28 | return $this->container->make('flamarkt.availability_driver_filters');
29 | });
30 |
31 | $this->container->instance('flamarkt.price_drivers', [
32 | 'fixed' => new PriceDriver,
33 | ]);
34 | $this->container->instance('flamarkt.price_driver_filters', []);
35 |
36 | $this->container->singleton(PriceManager::class);
37 | $this->container->when(PriceManager::class)
38 | ->needs('$drivers')
39 | ->give(function (): array {
40 | return $this->container->make('flamarkt.price_drivers');
41 | });
42 | $this->container->when(PriceManager::class)
43 | ->needs('$filters')
44 | ->give(function (): array {
45 | return $this->container->make('flamarkt.price_driver_filters');
46 | });
47 | }
48 |
49 | public function boot()
50 | {
51 | Product::setFormatter($this->container->make('flarum.formatter'));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Product/PriceManager.php:
--------------------------------------------------------------------------------
1 | process($product, $actor, $request);
25 |
26 | $price = 0;
27 |
28 | foreach ($results as $result) {
29 | // Null values are ignored
30 | if (is_null($result)) {
31 | continue;
32 | }
33 |
34 | if (preg_match('~^([-+]?)([0-9]+)(%?)$~', $result, $matches) === 1) {
35 | $value = (int)$matches[2];
36 |
37 | if ($matches[3]) {
38 | if (!$matches[1]) {
39 | throw new \Exception('Invalid price modification "' . $result . '" (cannot use % without calculation)');
40 | }
41 |
42 | $value = $value / 100 * $price;
43 | }
44 |
45 | switch ($matches[1]) {
46 | case '+':
47 | $price += $value;
48 | break;
49 | case '-':
50 | $price -= $value;
51 | break;
52 | default:
53 | $price = $value;
54 | break;
55 | }
56 | } else {
57 | throw new \Exception('Invalid price modification "' . $result . '"');
58 | }
59 | }
60 |
61 | return round($price);
62 | }
63 |
64 | public function driver(Product $product, User $actor, ServerRequestInterface $request = null): string
65 | {
66 | return $product->price_driver ?? $this->settings->get('flamarkt.defaultPriceDriver') ?: 'fixed';
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/js/dist-typings/backoffice/compat.d.ts:
--------------------------------------------------------------------------------
1 | export const backoffice: {
2 | 'components/EditPaymentModal': typeof EditPaymentModal;
3 | 'components/OrderLineEdit': typeof OrderLineEdit;
4 | 'components/OrderList': typeof OrderList;
5 | 'components/OrderPaymentSection': typeof OrderPaymentSection;
6 | 'components/OrderRelationshipSelect': typeof OrderRelationshipSelect;
7 | 'components/PaymentList': typeof PaymentList;
8 | 'components/ProductList': typeof ProductList;
9 | 'components/ProductRelationshipSelect': typeof ProductRelationshipSelect;
10 | 'pages/OrderIndexPage': typeof OrderIndexPage;
11 | 'pages/OrderShowPage': typeof OrderShowPage;
12 | 'pages/ProductIndexPage': typeof ProductIndexPage;
13 | 'pages/ProductShowPage': typeof ProductShowPage;
14 | 'states/OrderLineEditState': typeof OrderLineEditState;
15 | 'states/PaymentListPassthroughState': typeof PaymentListPassthroughState;
16 | 'utils/augmentOptionsWithValue': typeof augmentOptionsWithValue;
17 | 'utils/OrderLineOptions': {
18 | orderLineGroupOptions(): {
19 | [key: string]: string;
20 | };
21 | orderLineTypeOptions(): {
22 | [key: string]: string;
23 | };
24 | };
25 | };
26 | import EditPaymentModal from "./components/EditPaymentModal";
27 | import OrderLineEdit from "./components/OrderLineEdit";
28 | import OrderList from "./components/OrderList";
29 | import OrderPaymentSection from "./components/OrderPaymentSection";
30 | import OrderRelationshipSelect from "./components/OrderRelationshipSelect";
31 | import PaymentList from "./components/PaymentList";
32 | import ProductList from "./components/ProductList";
33 | import ProductRelationshipSelect from "./components/ProductRelationshipSelect";
34 | import OrderIndexPage from "./pages/OrderIndexPage";
35 | import OrderShowPage from "./pages/OrderShowPage";
36 | import ProductIndexPage from "./pages/ProductIndexPage";
37 | import ProductShowPage from "./pages/ProductShowPage";
38 | import OrderLineEditState from "./states/OrderLineEditState";
39 | import PaymentListPassthroughState from "./states/PaymentListPassthroughState";
40 | import augmentOptionsWithValue from "./utils/augmentOptionsWithValue";
41 |
--------------------------------------------------------------------------------
/src/Api/Controller/OrderIndexController.php:
--------------------------------------------------------------------------------
1 | 'desc',
31 | ];
32 |
33 | public function __construct(
34 | protected OrderFilterer $filterer,
35 | protected OrderSearcher $searcher,
36 | protected UrlGenerator $url
37 | )
38 | {
39 | }
40 |
41 | protected function data(ServerRequestInterface $request, Document $document)
42 | {
43 | $actor = RequestUtil::getActor($request);
44 | $filters = $this->extractFilter($request);
45 | $sort = $this->extractSort($request);
46 |
47 | $limit = $this->extractLimit($request);
48 | $offset = $this->extractOffset($request);
49 | $include = $this->extractInclude($request);
50 |
51 | $criteria = new QueryCriteria($actor, $filters, $sort);
52 | if (array_key_exists('q', $filters)) {
53 | $results = $this->searcher->search($criteria, $limit, $offset);
54 | } else {
55 | $results = $this->filterer->filter($criteria, $limit, $offset);
56 | }
57 |
58 | $document->addPaginationLinks(
59 | $this->url->to('api')->route('flamarkt.orders.index'),
60 | $request->getQueryParams(),
61 | $offset,
62 | $limit,
63 | $results->areMoreResults() ? null : 0
64 | );
65 |
66 | $this->loadRelations($results->getResults(), $include);
67 |
68 | return $results->getResults();
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Order/OrderBuilder.php:
--------------------------------------------------------------------------------
1 | lines, $group ?? 'default')) {
36 | $this->lines[$group ?? 'default'] = [];
37 | }
38 |
39 | $line = new OrderLine();
40 | $line->group = $group;
41 | $line->type = $type;
42 |
43 | $this->lines[$group ?? 'default'][] = $line;
44 |
45 | return $line;
46 | }
47 |
48 | public function addPayment(string $method, int $amount, string $identifier = null): Payment
49 | {
50 | $payment = new Payment();
51 | $payment->method = $method;
52 | $payment->identifier = $identifier;
53 | $payment->amount = $amount;
54 |
55 | $this->payments[] = $payment;
56 |
57 | return $payment;
58 | }
59 |
60 | public function priceTotal(): int
61 | {
62 | $total = 0;
63 |
64 | foreach ($this->lines as $group => $lines) {
65 | foreach ($lines as $line) {
66 | $total += $line->price_total;
67 | }
68 | }
69 |
70 | return $total;
71 | }
72 |
73 | public function totalPaid(): int
74 | {
75 | $total = 0;
76 |
77 | foreach ($this->payments as $payment) {
78 | $total += $payment->amount;
79 | }
80 |
81 | return $total;
82 | }
83 |
84 | public function totalUnpaid(): int
85 | {
86 | return $this->priceTotal() - $this->totalPaid();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flamarkt/core",
3 | "description": "The minimal marketplace framework based on Flarum.",
4 | "keywords": [
5 | "flarum",
6 | "flamarkt",
7 | "marketplace",
8 | "ecommerce"
9 | ],
10 | "type": "flarum-extension",
11 | "license": "MIT",
12 | "require": {
13 | "php": "^8.1",
14 | "flarum/core": "^1.2",
15 | "flamarkt/backoffice": "^0.1.0",
16 | "clarkwinkelmann/flarum-mithril2html": "^1.0",
17 | "ext-json": "*"
18 | },
19 | "require-dev": {
20 | "phpunit/phpunit": "^9.5",
21 | "flarum/testing": "^1.0"
22 | },
23 | "authors": [
24 | {
25 | "name": "Flamarkt",
26 | "homepage": "https://flamarkt.org/"
27 | }
28 | ],
29 | "support": {
30 | "issues": "https://github.com/flamarkt/core/issues",
31 | "source": "https://github.com/flamarkt/core"
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "Flamarkt\\Core\\": "src/"
36 | }
37 | },
38 | "autoload-dev": {
39 | "psr-4": {
40 | "Flamarkt\\Core\\Tests\\": "tests/"
41 | }
42 | },
43 | "extra": {
44 | "flarum-extension": {
45 | "title": "Flamarkt",
46 | "category": "feature",
47 | "icon": {
48 | "name": "fas fa-shopping-cart",
49 | "backgroundColor": "#266a13",
50 | "color": "#fff"
51 | },
52 | "optional-dependencies": [
53 | "clarkwinkelmann/flarum-ext-scout"
54 | ]
55 | },
56 | "flamarkt-backoffice": {
57 | "settingsInBackoffice": true,
58 | "showInBackoffice": true
59 | },
60 | "branch-alias": {
61 | "dev-main": "0.1.x-dev"
62 | }
63 | },
64 | "scripts": {
65 | "test": [
66 | "@test:integration"
67 | ],
68 | "test:integration": "phpunit -c tests/phpunit.integration.xml",
69 | "test:setup": "@php tests/integration/setup.php"
70 | },
71 | "scripts-descriptions": {
72 | "test": "Runs all tests.",
73 | "test:integration": "Runs all integration tests.",
74 | "test:setup": "Sets up a database for use with integration tests. Execute this only once."
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Order/OrderLineValidator.php:
--------------------------------------------------------------------------------
1 | line;
16 | }
17 |
18 | public function setOrderLine(OrderLine $line = null)
19 | {
20 | $this->line = $line;
21 | }
22 |
23 | protected function getRules(): array
24 | {
25 | $groups = resolve('flamarkt.order.line.groups');
26 | $types = resolve('flamarkt.order.line.types');
27 |
28 | // Prevent errors if trying to save an invalid value that was already in the database
29 | // Otherwise it would be impossible to edit other lines or attributes of a broken order without fixing all problems at once
30 | // Which might not always be possible or desired
31 | if ($this->line) {
32 | if ($this->line->group) {
33 | $groups[] = $this->line->group;
34 | }
35 |
36 | if ($this->line->type) {
37 | $types[] = $this->line->type;
38 | }
39 | }
40 |
41 | return [
42 | 'group' => [
43 | 'nullable',
44 | Rule::in($groups),
45 | ],
46 | 'type' => [
47 | 'nullable',
48 | Rule::in($types),
49 | ],
50 | 'label' => [
51 | 'nullable',
52 | 'string',
53 | 'max:255',
54 | ],
55 | 'comment' => [
56 | 'nullable',
57 | 'string',
58 | 'max:255',
59 | ],
60 | 'productUid' => [
61 | Rule::exists('flamarkt_products', 'uid'),
62 | ],
63 | 'quantity' => [
64 | 'integer',
65 | 'min:-2147483648', // MySQL signed INT range
66 | 'max:2147483647',
67 | ],
68 | 'priceUnit' => [
69 | 'integer',
70 | 'min:-2147483648', // MySQL signed INT range
71 | 'max:2147483647',
72 | ],
73 | ];
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Api/Controller/ProductIndexController.php:
--------------------------------------------------------------------------------
1 | 'desc',
28 | ];
29 |
30 | public $limit = 24;
31 |
32 | public function __construct(
33 | protected ProductFilterer $filterer,
34 | protected ProductSearcher $searcher,
35 | protected UrlGenerator $url
36 | )
37 | {
38 | }
39 |
40 | protected function data(ServerRequestInterface $request, Document $document)
41 | {
42 | $actor = RequestUtil::getActor($request);
43 | $filters = $this->extractFilter($request);
44 | $sort = $this->extractSort($request);
45 |
46 | $limit = $this->extractLimit($request);
47 | $offset = $this->extractOffset($request);
48 | $include = array_merge($this->extractInclude($request), ['cartState', 'userState']);
49 |
50 | $criteria = new QueryCriteria($actor, $filters, $sort);
51 | if (array_key_exists('q', $filters)) {
52 | $results = $this->searcher->search($criteria, $limit, $offset);
53 | } else {
54 | $results = $this->filterer->filter($criteria, $limit, $offset);
55 | }
56 |
57 | $document->addPaginationLinks(
58 | $this->url->to('api')->route('flamarkt.products.index'),
59 | $request->getQueryParams(),
60 | $offset,
61 | $limit,
62 | $results->areMoreResults() ? null : 0
63 | );
64 |
65 | Product::setStateUser($actor);
66 | Product::setStateCart($request->getAttribute('cart'));
67 |
68 | $this->loadRelations($results->getResults(), $include);
69 |
70 | return $results->getResults();
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/views/email/template.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
61 |
62 |
63 |
68 |
69 | @yield('content')
70 |
71 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/Extend/Mail.php:
--------------------------------------------------------------------------------
1 | templateView = $view;
26 |
27 | return $this;
28 | }
29 |
30 | /**
31 | * Change the primary color used for the email inline CSS.
32 | * @param string|callable $callback A callback or invokable ::class that returns a string that can be inserted as a CSS color.
33 | * @return $this
34 | */
35 | public function themeColor($callback): self
36 | {
37 | $this->themeColor = $callback;
38 |
39 | return $this;
40 | }
41 |
42 | /**
43 | * Change the logo/link at the top of the email.
44 | * @param string|callable $callback A callback or invokable ::class that returns a string of HTML.
45 | * @return $this
46 | */
47 | public function brand($callback): self
48 | {
49 | $this->brand = $callback;
50 |
51 | return $this;
52 | }
53 |
54 | /**
55 | * Add more CSS to the email inline style block.
56 | * @param string|callable $callback A callback or invokable ::class that returns a string of CSS or null. The class will receive BlueprintInterface|null $blueprint as first argument.
57 | * @return $this
58 | */
59 | public function css($callback): self
60 | {
61 | $this->css[] = $callback;
62 |
63 | return $this;
64 | }
65 |
66 | public function extend(Container $container, Extension $extension = null)
67 | {
68 | if ($this->templateView) {
69 | MailConfig::$templateView = $this->templateView;
70 | }
71 |
72 | if ($this->themeColor) {
73 | MailConfig::$themeColorCallback = ContainerUtil::wrapCallback($this->themeColor, $container);
74 | }
75 |
76 | if ($this->brand) {
77 | MailConfig::$brandCallback = ContainerUtil::wrapCallback($this->brand, $container);
78 | }
79 |
80 | foreach ($this->css as $css) {
81 | MailConfig::$cssCallbacks[] = ContainerUtil::wrapCallback($css, $container);
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Cart/Cart.php:
--------------------------------------------------------------------------------
1 | 'int',
43 | 'price_total' => 'int',
44 | ];
45 |
46 | public function user(): Relations\BelongsTo
47 | {
48 | return $this->belongsTo(User::class);
49 | }
50 |
51 | public function products(): Relations\BelongsToMany
52 | {
53 | // Hard-code cart_id column name to prevent GuestCart from trying to use a different auto-generated name
54 | return $this->belongsToMany(Product::class, 'flamarkt_cart_product', 'cart_id')
55 | // We inject a visibility scope here to allow extensions to customize the relationship
56 | // However the actor is not available, it will always be guest
57 | // This is mostly for changing the sort order and optionally creating fully invisible products
58 | ->whereVisibleTo(new Guest(), 'cart')
59 | ->withPivot('quantity');
60 | }
61 |
62 | public function updateMeta(): void
63 | {
64 | $this->product_count = $this->products()->count();
65 |
66 | try {
67 | $builder = resolve(OrderBuilderFactory::class)->pretend($this);
68 | $this->price_total = $builder->priceTotal();
69 | $this->amount_due_after_partial = $builder->totalUnpaid();
70 | } catch (\Exception $exception) {
71 | $this->price_total = null;
72 | $this->amount_due_after_partial = null;
73 | }
74 |
75 | $this->save();
76 | }
77 |
78 | public function getAlreadySubmittedAttribute(): bool
79 | {
80 | return !is_null($this->order_id);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Extend/Payment.php:
--------------------------------------------------------------------------------
1 | pretend is true, partial payment providers should add a payment record but not actually persist the transaction.
19 | *
20 | * @param callable|string $callback
21 | *
22 | * The callback can be a closure or invokable class, and should accept:
23 | * - OrderBuilder $builder
24 | * - Order $order
25 | * - User $actor
26 | * - Cart $cart
27 | * - array $data
28 | *
29 | * @return self
30 | */
31 | public function partialCallback($callback): self
32 | {
33 | $this->partialCallbacks[] = $callback;
34 |
35 | return $this;
36 | }
37 |
38 | /**
39 | * Register a payment callback that should execute after partial payment options.
40 | * This is intended for methods that will cover the full remaining unpaid amount.
41 | * Callbacks should default to no-op if $builder->pretend is true.
42 | *
43 | * @param callable|string $callback
44 | *
45 | * The callback can be a closure or invokable class, and should accept:
46 | * - OrderBuilder $builder
47 | * - Order $order
48 | * - User $actor
49 | * - Cart $cart
50 | * - array $data
51 | *
52 | * @return self
53 | */
54 | public function remainingCallback($callback): self
55 | {
56 | $this->partialCallbacks[] = $callback;
57 |
58 | return $this;
59 | }
60 |
61 | public function extend(Container $container, Extension $extension = null)
62 | {
63 | $container->extend('flamarkt.payment.callbacks', function ($callbacks) use ($container) {
64 | $callbacks['partial'] = array_merge($callbacks['partial'], array_map(function ($callback) use ($container) {
65 | return ContainerUtil::wrapCallback($callback, $container);
66 | }, $this->partialCallbacks));
67 | $callbacks['remaining'] = array_merge($callbacks['remaining'], array_map(function ($callback) use ($container) {
68 | return ContainerUtil::wrapCallback($callback, $container);
69 | }, $this->remainingCallbacks));
70 |
71 | return $callbacks;
72 | });
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Order/Order.php:
--------------------------------------------------------------------------------
1 | 'int',
44 | 'paid_amount' => 'int',
45 | 'created_at' => 'datetime',
46 | 'updated_at' => 'datetime',
47 | 'hidden_at' => 'datetime',
48 | ];
49 |
50 | public function user(): Relations\BelongsTo
51 | {
52 | return $this->belongsTo(User::class);
53 | }
54 |
55 | public function lines(): Relations\HasMany
56 | {
57 | return $this->hasMany(OrderLine::class);
58 | }
59 |
60 | public function payments(): Relations\HasMany
61 | {
62 | return $this->hasMany(Payment::class);
63 | }
64 |
65 | public function products(): Relations\BelongsToMany
66 | {
67 | return $this->belongsToMany(Product::class, 'flamarkt_order_lines');
68 | }
69 |
70 | public function hide(): self
71 | {
72 | if (!$this->hidden_at) {
73 | $this->hidden_at = Carbon::now();
74 |
75 | $this->raise(new Hidden($this));
76 | }
77 |
78 | return $this;
79 | }
80 |
81 | public function restore(): self
82 | {
83 | if ($this->hidden_at !== null) {
84 | $this->hidden_at = null;
85 |
86 | $this->raise(new Restored($this));
87 | }
88 |
89 | return $this;
90 | }
91 |
92 | public function updateMeta(): self
93 | {
94 | $this->price_total = $this->lines()->sum('price_total');
95 | $this->paid_amount = $this->payments()->sum('amount');
96 | $this->product_count = $this->products()->count();
97 |
98 | return $this;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/js/dist/mithril2html.js:
--------------------------------------------------------------------------------
1 | (()=>{var r={n:t=>{var o=t&&t.__esModule?()=>t.default:()=>t;return r.d(o,{a:o}),o},d:(t,o)=>{for(var e in o)r.o(o,e)&&!r.o(t,e)&&Object.defineProperty(t,e,{enumerable:!0,get:o[e]})},o:(r,t)=>Object.prototype.hasOwnProperty.call(r,t),r:r=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(r,"__esModule",{value:!0})}},t={};(()=>{"use strict";r.r(t),r.d(t,{mithril2html:()=>g});const o=flarum.core.compat["forum/app"];var e=r.n(o);function n(r,t){return n=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(r,t){return r.__proto__=t,r},n(r,t)}function a(r,t){r.prototype=Object.create(t.prototype),r.prototype.constructor=r,n(r,t)}const c=flarum.core.compat["common/components/Page"];var i=r.n(c);const u=((flarum.extensions["flamarkt-core"]||{}).forum||{})["components/OrderFacts"];var d=r.n(u);const s=((flarum.extensions["flamarkt-core"]||{}).forum||{})["components/OrderTable"];var p=r.n(s);const l=flarum.core.compat["common/utils/ItemList"];var f=r.n(l);const h=flarum.core.compat["common/components/LinkButton"];var y=r.n(h),v=function(r){function t(){for(var t,o=arguments.length,e=new Array(o),n=0;n {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/components/BasicsPage'];","import {extend} from 'flarum/common/extend';\nimport BasicsPage from 'flarum/admin/components/BasicsPage';\n\napp.initializers.add('flamarkt-core', () => {\n extend(BasicsPage.prototype, 'homePageItems', function (items) {\n items.add('flamarkt-products', {\n path: '/products',\n label: app.translator.trans('flamarkt-core.admin.homepage.products'),\n });\n });\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","core","compat","app","initializers","add","extend","BasicsPage","items","path","label","translator","trans"],"sourceRoot":""}
--------------------------------------------------------------------------------
/src/Api/Serializer/ProductSerializer.php:
--------------------------------------------------------------------------------
1 | $product->description,
27 | 'descriptionHtml' => $product->formatDescription($this->request),
28 | 'canOrder' => $this->availability->canOrder($product, $this->actor, $this->request),
29 | 'price' => $this->price->price($product, $this->actor, $this->request),
30 | ];
31 |
32 | // canOrder was renamed to canAddToCart for clarity
33 | // We keep the original for backward compatibility and also because it might be implemented with a different meaning in the future
34 | $attributes['canAddToCart'] = $attributes['canOrder'];
35 |
36 | // We can't pre-load the correct cart from here since we don't have access to the request
37 | // Instead the cart should always be set in the controller that might return products that require it
38 | if ($state = $product->cartState) {
39 | $attributes += [
40 | 'cartQuantity' => $state->quantity,
41 | ];
42 | }
43 |
44 | if ($this->actor->can('backoffice')) {
45 | $attributes += [
46 | 'canEdit' => true,
47 | 'priceEdit' => $product->price,
48 | 'availabilityDriver' => $product->availability_driver,
49 | 'priceDriver' => $product->price_driver,
50 | 'createdAt' => $this->formatDate($product->created_at),
51 | 'updatedAt' => $this->formatDate($product->updated_at),
52 | 'hiddenAt' => $this->formatDate($product->hidden_at),
53 | ];
54 | }
55 |
56 | if ($product->hidden_at) {
57 | $attributes['isHidden'] = true;
58 | }
59 |
60 | // Core does not include any relationship on that state, but we take care of the loading
61 | Product::setStateUser($this->actor);
62 |
63 | return $attributes;
64 | }
65 |
66 | public function cart($product): ?Relationship
67 | {
68 | // It was supposed to work with just $product->cart and the regular relation definition
69 | // But I can't figure out why the relation value is NULL in the serializer while perfectly fine in the controller
70 | $data = $product->getCartFromState();
71 |
72 | if ($data) {
73 | $serializer = $this->resolveSerializer(CartSerializer::class, $product, $data);
74 |
75 | return new Relationship(new Resource($data, $serializer));
76 | }
77 |
78 | return null;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Cart/CartLock.php:
--------------------------------------------------------------------------------
1 | isSubmitLocked($cart)) {
25 | return 'SUBMIT';
26 | }
27 |
28 | // Lock expiration is taken care of by the cache driver
29 | $origin = $this->cache->get($cart->uid . '-content-lock');
30 |
31 | // No need for a special NULL check since NULL will never be stored in this cache value (`default` is used)
32 | if ($origin) {
33 | return $origin;
34 | }
35 |
36 | return null;
37 | }
38 |
39 | public function isContentLocked(Cart $cart): bool
40 | {
41 | return $this->getContentLockOrigin($cart) !== null;
42 | }
43 |
44 | public function isContentLockedExceptBy(Cart $cart, string $exceptOrigin): bool
45 | {
46 | $origin = $this->getContentLockOrigin($cart);
47 |
48 | return $origin === 'SUBMIT' || ($origin && $origin !== $exceptOrigin);
49 | }
50 |
51 | public function isContentLockedBy(Cart $cart, string $origin): bool
52 | {
53 | return $this->getContentLockOrigin($cart) === $origin;
54 | }
55 |
56 | public function isSubmitLocked(Cart $cart): bool
57 | {
58 | // Lock expiration is taken care of by the cache driver
59 | return $this->cache->has($cart->uid . '-submit-lock');
60 | }
61 |
62 | public function lockContent(Cart $cart, string $origin = 'default'): void
63 | {
64 | // The lock lifetime is to prevent submitting the cart again too soon if Flamarkt
65 | // experienced an unrecoverable error during processing and wasn't able to clear the lock at the end
66 | // There likely isn't any queued task at this point, so it's just to let any error handler finish its job
67 | $this->cache->set($cart->uid . '-content-lock', $origin, 120); // 2 minutes
68 | }
69 |
70 | public function lockSubmit(Cart $cart): void
71 | {
72 | // The lock lifetime is to prevent submitting the cart again too soon if Flamarkt
73 | // experienced an unrecoverable error during processing and wasn't able to clear the lock at the end
74 | // There likely isn't any queued task at this point, so it's just to let any error handler finish its job
75 | $this->cache->set($cart->uid . '-submit-lock', 'default', 120); // 2 minutes
76 | }
77 |
78 | public function unlockContent(Cart $cart): void
79 | {
80 | $this->cache->forget($cart->uid . '-content-lock');
81 | }
82 |
83 | public function unlockSubmit(Cart $cart): void
84 | {
85 | $this->unlockContent($cart);
86 | $this->cache->forget($cart->uid . '-submit-lock');
87 | }
88 | }
89 |
--------------------------------------------------------------------------------