├── 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 |
4 |

{{ $translator->trans('flamarkt-core.forum.products.headingTitle') }}

5 | 6 | 17 | 18 | @if (isset($apiDocument->links->prev)) 19 | « {{ $translator->trans('core.views.index.previous_page_button') }} 20 | @endif 21 | 22 | @if (isset($apiDocument->links->next)) 23 | {{ $translator->trans('core.views.index.next_page_button') }} » 24 | @endif 25 |
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 |
64 |
65 | {!! \Flamarkt\Core\Notification\MailConfig::brand() !!} 66 |
67 |
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 | --------------------------------------------------------------------------------