Product.php 85 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424
  1. <?php
  2. namespace Webkul\BagistoApi\Models;
  3. use ApiPlatform\Metadata\ApiProperty;
  4. use ApiPlatform\Metadata\ApiResource;
  5. use ApiPlatform\Metadata\Get;
  6. use ApiPlatform\Metadata\GraphQl\Mutation;
  7. use ApiPlatform\Metadata\GraphQl\Query;
  8. use ApiPlatform\Metadata\GraphQl\QueryCollection;
  9. use ApiPlatform\Metadata\Post;
  10. use ApiPlatform\Metadata\Put;
  11. use ApiPlatform\OpenApi\Model;
  12. use Illuminate\Database\Eloquent\Relations\BelongsTo;
  13. use Illuminate\Database\Eloquent\Relations\BelongsToMany;
  14. use Illuminate\Database\Eloquent\Relations\HasMany;
  15. use Symfony\Component\Serializer\Annotation\Groups;
  16. use Webkul\BagistoApi\Http\Requests\Admin\ProductFormRequest;
  17. use Webkul\BagistoApi\Resolver\SingleProductBagistoApiResolver;
  18. use Webkul\BagistoApi\State\ProductBagistoApiProvider;
  19. use Webkul\BagistoApi\State\ProductGraphQLProvider;
  20. use Webkul\BagistoApi\State\ProductProcessor;
  21. use Webkul\Product\Models\Product as BaseProduct;
  22. use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
  23. use Longyi\Core\Models\ProductOption;
  24. use Longyi\Core\Models\ProductOptionValue;
  25. #[ApiResource(
  26. routePrefix: '/api/shop',
  27. operations: [
  28. new Get,
  29. new Post(
  30. security: "is_granted('CREATE_PRODUCT')",
  31. processor: ProductProcessor::class,
  32. // rules: ProductFormRequest::class,
  33. openapi: new Model\Operation(
  34. summary: 'Store the product',
  35. description: 'Product creation endpoint',
  36. tags: ['Product'],
  37. parameters: [],
  38. requestBody: new Model\RequestBody(
  39. description: 'Product creation payload',
  40. required: true,
  41. content: new \ArrayObject([
  42. 'application/json' => [
  43. 'schema' => [
  44. 'type' => 'object',
  45. 'properties' => [
  46. 'type' => [
  47. 'type' => 'string',
  48. 'example' => 'simple',
  49. ],
  50. 'attribute_family_id' => [
  51. 'type' => 'integer',
  52. 'example' => 1,
  53. ],
  54. 'sku' => [
  55. 'type' => 'string',
  56. 'example' => 'furniture',
  57. ],
  58. 'super_attributes' => [
  59. 'type' => 'object',
  60. 'example' => [
  61. 'color' => [1],
  62. 'size' => [6],
  63. ],
  64. ],
  65. ],
  66. ],
  67. 'examples' => [
  68. 'simple_product' => [
  69. 'summary' => 'Simple Product',
  70. 'description' => 'Create a standard simple product',
  71. 'value' => [
  72. 'type' => 'simple',
  73. 'attribute_family_id' => 1,
  74. 'sku' => 'furniture',
  75. ],
  76. ],
  77. 'configurable_product' => [
  78. 'summary' => 'Configurable Product',
  79. 'description' => 'Create a configurable product with variations',
  80. 'value' => [
  81. 'type' => 'configurable',
  82. 'attribute_family_id' => 1,
  83. 'sku' => 'furniture',
  84. 'super_attributes' => [
  85. 'color' => [1],
  86. 'size' => [6],
  87. ],
  88. ],
  89. ],
  90. ],
  91. ],
  92. ]),
  93. ),
  94. ),
  95. ),
  96. new Put(
  97. security: "is_granted('EDIT_PRODUCT')",
  98. processor: ProductProcessor::class,
  99. openapi: new Model\Operation(
  100. summary: 'Update the product',
  101. description: 'Product update endpoint',
  102. tags: ['Product'],
  103. parameters: [],
  104. requestBody: new Model\RequestBody(
  105. description: 'Product update payload',
  106. required: true,
  107. content: new \ArrayObject([
  108. 'application/json' => [
  109. 'schema' => [
  110. 'type' => 'object',
  111. 'properties' => [
  112. 'type' => [
  113. 'type' => 'string',
  114. 'example' => 'simple',
  115. ],
  116. 'attribute_family_id' => [
  117. 'type' => 'integer',
  118. 'example' => 1,
  119. ],
  120. 'sku' => [
  121. 'type' => 'string',
  122. 'example' => 'furniture',
  123. ],
  124. 'super_attributes' => [
  125. 'type' => 'object',
  126. 'example' => [
  127. 'color' => [1],
  128. 'size' => [6],
  129. ],
  130. ],
  131. ],
  132. ],
  133. 'examples' => [
  134. 'simple_product' => [
  135. 'summary' => 'Simple Product',
  136. 'description' => 'Create a standard simple product',
  137. 'value' => [
  138. 'channel' => 'default',
  139. 'locale' => 'en',
  140. 'sku' => 'furniture',
  141. 'product_number' => 'ssf-001',
  142. 'name' => 'Sofa Set',
  143. 'url_key' => 'sofa-set-furniture',
  144. 'tax_category_id' => null,
  145. 'new' => 1,
  146. 'featured' => 1,
  147. 'manage_stock' => 1,
  148. 'visible_individually' => 1,
  149. 'guest_checkout' => 0,
  150. 'status' => 1,
  151. 'color' => 3,
  152. 'size' => 9,
  153. 'brand' => 17,
  154. 'short_description' => 'What is Lorem Ipsum?',
  155. 'description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
  156. 'meta_title' => 'Premium sofa sets',
  157. 'meta_description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
  158. 'meta_keywords' => 'Sofa set',
  159. 'price' => 10.5,
  160. 'cost' => 0,
  161. 'special_price' => 8.3,
  162. 'special_price_from' => '2023-05-30',
  163. 'special_price_to' => '2025-05-22',
  164. 'length' => 0,
  165. 'width' => 0,
  166. 'height' => 0,
  167. 'weight' => 1,
  168. 'inventories' => [
  169. '1' => 500,
  170. ],
  171. 'images' => [
  172. 'files' => [],
  173. 'position' => [1],
  174. ],
  175. 'videos' => [
  176. 'files' => [],
  177. 'position' => [1],
  178. ],
  179. 'categories' => [1],
  180. 'channels' => [1],
  181. 'up_sell' => [1],
  182. 'cross_sell' => [1],
  183. 'related_products' => [1],
  184. ],
  185. ],
  186. 'configurable_product' => [
  187. 'summary' => 'Configurable Product',
  188. 'description' => 'Update a configurable product with variations',
  189. 'value' => [
  190. 'channel' => 'default',
  191. 'locale' => 'en',
  192. 'sku' => 'skipping-rope',
  193. 'product_number' => 'sr-001',
  194. 'name' => 'Skipping Rope',
  195. 'url_key' => 'skipping-rope',
  196. 'tax_category_id' => null,
  197. 'new' => 1,
  198. 'featured' => 1,
  199. 'visible_individually' => 1,
  200. 'guest_checkout' => 0,
  201. 'status' => 1,
  202. 'brand' => 17,
  203. 'short_description' => 'What is Lorem Ipsum?',
  204. 'description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
  205. 'meta_title' => 'Premium sofa sets',
  206. 'meta_description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
  207. 'meta_keywords' => 'Sofa set',
  208. 'price' => 0,
  209. 'customer_group_prices' => [
  210. 'customer_group_price_0' => [
  211. 'customer_group_id' => 1,
  212. 'qty' => 2,
  213. 'value_type' => 'fixed',
  214. 'value' => 3.2,
  215. ],
  216. ],
  217. 'categories' => [
  218. 1,
  219. 2,
  220. ],
  221. 'channels' => [
  222. 1,
  223. 3,
  224. 4,
  225. ],
  226. 'variants' => [
  227. '28' => [
  228. 'sku' => 'skipping-rope-variant-1-6',
  229. 'name' => 'Red-S',
  230. 'color' => 1,
  231. 'size' => 6,
  232. 'price' => 10.5,
  233. 'weight' => 1.2,
  234. 'status' => 1,
  235. 'inventories' => [
  236. '1' => 500,
  237. ],
  238. 'images[]' => [
  239. 'string',
  240. ],
  241. ],
  242. '29' => [
  243. 'sku' => 'skipping-rope-variant-1-7',
  244. 'name' => 'Red-M',
  245. 'color' => 1,
  246. 'size' => 7,
  247. 'price' => 15,
  248. 'weight' => 1,
  249. 'status' => 1,
  250. 'inventories' => [
  251. '1' => 500,
  252. ],
  253. 'images[files]' => [
  254. 'string',
  255. ],
  256. ],
  257. ],
  258. ],
  259. ],
  260. 'downloadable_product' => [
  261. 'summary' => 'Downloadable Product',
  262. 'description' => 'Update a downloadable product with links and samples',
  263. 'value' => [
  264. 'channel' => 'default',
  265. 'locale' => 'en',
  266. 'sku' => 'skipping-rope',
  267. 'product_number' => 'sr-001',
  268. 'name' => 'Skipping Rope',
  269. 'url_key' => 'skipping-rope',
  270. 'tax_category_id' => null,
  271. 'new' => 1,
  272. 'featured' => 1,
  273. 'visible_individually' => 1,
  274. 'guest_checkout' => 0,
  275. 'status' => 1,
  276. 'brand' => 17,
  277. 'short_description' => 'What is Lorem Ipsum?',
  278. 'description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
  279. 'meta_title' => 'Premium sofa sets',
  280. 'meta_description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
  281. 'meta_keywords' => 'Sofa set',
  282. 'price' => 0,
  283. 'customer_group_prices' => [
  284. 'customer_group_price_0' => [
  285. 'customer_group_id' => 1,
  286. 'qty' => 2,
  287. 'value_type' => 'fixed',
  288. 'value' => 3.2,
  289. ],
  290. ],
  291. 'categories' => [
  292. 1,
  293. 2,
  294. ],
  295. 'channels' => [
  296. 1,
  297. 3,
  298. 4,
  299. ],
  300. 'downloadable_links' => [
  301. 'link_0' => [
  302. 'en' => [
  303. 'title' => 'Link 1',
  304. ],
  305. 'price' => 5,
  306. 'type' => 'url',
  307. 'url' => 'https://cdn.pixabay.com/photo/2016/03/26/13/08/conceptual-1280533_1280.jpg',
  308. 'sample_type' => 'url',
  309. 'sample_url' => 'https://cdn.pixabay.com/photo/2016/11/22/19/11/brick-wall-1850095_1280.jpg',
  310. 'downloads' => 10,
  311. 'sort_order' => 1,
  312. ],
  313. 'link_1' => [
  314. 'en' => [
  315. 'title' => 'Link 2',
  316. ],
  317. 'price' => 10,
  318. 'type' => 'url',
  319. 'url' => 'https://cdn.pixabay.com/photo/2016/03/26/13/08/conceptual-1280533_1280.jpg',
  320. 'sample_type' => 'url',
  321. 'sample_url' => 'https://cdn.pixabay.com/photo/2016/11/22/19/11/brick-wall-1850095_1280.jpg',
  322. 'downloads' => 20,
  323. 'sort_order' => 2,
  324. ],
  325. ],
  326. 'downloadable_samples' => [
  327. 'sample_0' => [
  328. 'en' => [
  329. 'title' => 'Sample 1',
  330. ],
  331. 'type' => 'url',
  332. 'url' => 'https://cdn.pixabay.com/photo/2017/10/04/14/50/staging-2816464_1280.jpg',
  333. 'sort_order' => 1,
  334. ],
  335. 'sample_1' => [
  336. 'en' => [
  337. 'title' => 'Sample 2',
  338. ],
  339. 'type' => 'url',
  340. 'url' => 'https://cdn.pixabay.com/photo/2015/12/05/23/38/nursery-1078923_1280.jpg',
  341. 'sort_order' => 2,
  342. ],
  343. ],
  344. ],
  345. ],
  346. 'grouped_product' => [
  347. 'summary' => 'Group Product',
  348. 'description' => 'Update a grouped product along with its associated products',
  349. 'value' => [
  350. 'channel' => 'default',
  351. 'locale' => 'en',
  352. 'sku' => 'skipping-rope',
  353. 'product_number' => 'sr-001',
  354. 'name' => 'Skipping Rope',
  355. 'url_key' => 'skipping-rope',
  356. 'tax_category_id' => null,
  357. 'new' => 1,
  358. 'featured' => 1,
  359. 'visible_individually' => 1,
  360. 'guest_checkout' => 0,
  361. 'status' => 1,
  362. 'brand' => 17,
  363. 'short_description' => 'What is Lorem Ipsum?',
  364. 'description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
  365. 'meta_title' => 'Premium sofa sets',
  366. 'meta_description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
  367. 'meta_keywords' => 'Sofa set',
  368. 'price' => 0,
  369. 'customer_group_prices' => [
  370. 'customer_group_price_0' => [
  371. 'customer_group_id' => 1,
  372. 'qty' => 2,
  373. 'value_type' => 'fixed',
  374. 'value' => 3.2,
  375. ],
  376. ],
  377. 'categories' => [
  378. 1,
  379. 2,
  380. ],
  381. 'channels' => [
  382. 1,
  383. 3,
  384. 4,
  385. ],
  386. 'links' => [
  387. 'link_0' => [
  388. 'associated_product_id' => 1,
  389. 'qty' => 2,
  390. 'sort_order' => 1,
  391. ],
  392. 'link_1' => [
  393. 'associated_product_id' => 2,
  394. 'qty' => 3,
  395. 'sort_order' => 2,
  396. ],
  397. ],
  398. ],
  399. ],
  400. ],
  401. ],
  402. ]),
  403. ),
  404. ),
  405. ),
  406. ],
  407. graphQlOperations: [
  408. new Mutation(
  409. name: 'create',
  410. processor: \Webkul\BagistoApi\State\ProductProcessor::class,
  411. denormalizationContext: [
  412. 'allow_extra_attributes' => true,
  413. 'groups' => ['mutation'],
  414. ],
  415. ),
  416. new Mutation(
  417. name: 'update',
  418. processor: \Webkul\BagistoApi\State\ProductProcessor::class,
  419. ),
  420. new Query(
  421. args: [
  422. 'id' => ['type' => 'ID'],
  423. 'sku' => ['type' => 'String'],
  424. 'urlKey' => ['type' => 'String'],
  425. 'locale' => ['type' => 'String', 'description' => 'Locale code for localized data (e.g. "en", "fr")'],
  426. 'channel' => ['type' => 'String', 'description' => 'Channel code (e.g. "default")'],
  427. ],
  428. resolver: SingleProductBagistoApiResolver::class
  429. ),
  430. ]
  431. )]
  432. #[ApiResource(
  433. routePrefix: '/api/shop',
  434. shortName: 'Product',
  435. uriTemplate: '/products-collection',
  436. operations: [
  437. ],
  438. graphQlOperations: [
  439. new Query(resolver: BaseQueryItemResolver::class),
  440. new QueryCollection(
  441. provider: ProductGraphQLProvider::class,
  442. args: [
  443. 'sortKey' => [
  444. 'type' => 'String',
  445. 'description' => 'Sort products by field (TITLE, CREATED_AT, UPDATED_AT, PRICE, etc.)',
  446. ],
  447. 'reverse' => [
  448. 'type' => 'Boolean',
  449. 'description' => 'Reverse the sort order (true = descending, false = ascending)',
  450. ],
  451. 'query' => [
  452. 'type' => 'String',
  453. 'description' => 'Search query to filter products by SKU or name',
  454. ],
  455. 'filter' => [
  456. 'type' => 'String',
  457. 'description' => 'JSON filter object containing attribute filters (type, sku, category_id, price, color, name, etc.). Example: {"type":"configurable","sku":"ABC123"}',
  458. ],
  459. 'first' => [
  460. 'type' => 'Int',
  461. 'description' => 'Limit the number of products returned',
  462. ],
  463. 'after' => [
  464. 'type' => 'String',
  465. 'description' => 'Relay cursor for forward pagination',
  466. ],
  467. 'before' => [
  468. 'type' => 'String',
  469. 'description' => 'Relay cursor for backward pagination',
  470. ],
  471. 'last' => [
  472. 'type' => 'Int',
  473. 'description' => 'Return the last N items (used with before cursor)',
  474. ],
  475. 'locale' => [
  476. 'type' => 'String',
  477. 'description' => 'Fetch data products by locale',
  478. ],
  479. 'channel' => [
  480. 'type' => 'String',
  481. 'description' => 'Fetch data products by channel',
  482. ],
  483. ]
  484. ),
  485. ]
  486. )]
  487. class Product extends BaseProduct
  488. {
  489. public $locale;
  490. public $channel;
  491. /**
  492. * Keep the polymorphic type aligned with the base class used by the
  493. * Price indexer (Webkul\Product\Models\Product), so morphMany relations
  494. * like price_indices() resolve the rows that were written with
  495. * priceable_type = Webkul\Product\Models\Product.
  496. */
  497. public function getMorphClass(): string
  498. {
  499. return BaseProduct::class;
  500. }
  501. protected $with = [
  502. 'attribute_family',
  503. 'images',
  504. 'attribute_values',
  505. 'super_attributes',
  506. 'variants',
  507. 'price_indices',
  508. 'flexibleVariants',
  509. 'options',
  510. ];
  511. protected $appends = [
  512. 'name', 'description', 'short_description', 'price', 'special_price',
  513. 'weight', 'product_number', 'status', 'new', 'featured',
  514. 'visible_individually', 'guest_checkout', 'manage_stock',
  515. 'url_key', 'tax_category_id', 'special_price_from', 'special_price_to',
  516. 'meta_title', 'meta_keywords',
  517. 'cost', 'meta_description', 'length', 'width', 'height',
  518. 'color', 'size', 'brand', 'locale', 'channel', 'description_html',
  519. 'minimum_price', 'maximum_price', 'regular_minimum_price', 'regular_maximum_price',
  520. 'formatted_price', 'formatted_special_price', 'formatted_minimum_price',
  521. 'formatted_maximum_price', 'formatted_regular_minimum_price', 'formatted_regular_maximum_price',
  522. 'index', 'combinations', 'super_attribute_options',
  523. 'product_options',
  524. ];
  525. protected static array $systemAttributes = [
  526. 'sku' => ['id' => 1],
  527. 'name' => ['id' => 2],
  528. 'url_key' => ['id' => 3],
  529. 'tax_category_id' => ['id' => 4],
  530. 'new' => ['id' => 5],
  531. 'featured' => ['id' => 6],
  532. 'visible_individually' => ['id' => 7],
  533. 'status' => ['id' => 8],
  534. 'short_description' => ['id' => 9],
  535. 'description' => ['id' => 10],
  536. 'price' => ['id' => 11],
  537. 'special_price' => ['id' => 13],
  538. 'special_price_from' => ['id' => 14],
  539. 'special_price_to' => ['id' => 15],
  540. 'meta_title' => ['id' => 16],
  541. 'meta_keywords' => ['id' => 17],
  542. 'weight' => ['id' => 22],
  543. 'guest_checkout' => ['id' => 26],
  544. 'product_number' => ['id' => 27],
  545. 'manage_stock' => ['id' => 28],
  546. 'cost' => ['id' => 12],
  547. 'meta_description' => ['id' => 18],
  548. 'length' => ['id' => 19],
  549. 'width' => ['id' => 20],
  550. 'height' => ['id' => 21],
  551. 'color' => ['id' => 23],
  552. 'size' => ['id' => 24],
  553. 'brand' => ['id' => 25],
  554. ];
  555. #[ApiProperty(identifier: true, writable: false)]
  556. public function getId(): ?int
  557. {
  558. return $this->id;
  559. }
  560. /** Parent */
  561. #[ApiProperty(writable: true, readable: true, required: false)]
  562. public function getParent(): mixed
  563. {
  564. return $this->parent_id;
  565. }
  566. public function setParent(mixed $value): void
  567. {
  568. // Parent cannot be modified via API
  569. }
  570. /** Is Saleable */
  571. #[ApiProperty(
  572. writable: false,
  573. readable: true
  574. )]
  575. public function getIsSaleableAttribute(): bool
  576. {
  577. return parent::isSaleable();
  578. }
  579. public function isSaleable(): bool
  580. {
  581. return $this->getIsSaleableAttribute();
  582. }
  583. public function availableForSaleAttribute(): bool
  584. {
  585. return $this->status && $this->isSaleable();
  586. }
  587. /** Available for Sale */
  588. #[ApiProperty(
  589. writable: false,
  590. readable: true
  591. )]
  592. public function availableForSale(): bool
  593. {
  594. return $this->status && $this->isSaleable();
  595. }
  596. public function attribute_values(): HasMany
  597. {
  598. return $this->hasMany(AttributeValue::class, 'product_id');
  599. }
  600. /**
  601. * Get locale context.
  602. */
  603. /**
  604. * Get locale attribute.
  605. */
  606. public function getLocaleAttribute(): ?string
  607. {
  608. return $this->locale;
  609. }
  610. /**
  611. * Get locale context.
  612. */
  613. #[ApiProperty(
  614. writable: true,
  615. readable: true
  616. )]
  617. #[Groups(['mutation'])]
  618. public function getLocale(): ?string
  619. {
  620. return $this->getLocaleAttribute();
  621. }
  622. /**
  623. * Set locale context.
  624. */
  625. public function setLocale(?string $value): void
  626. {
  627. $this->locale = $value;
  628. }
  629. /**
  630. * Get channel attribute.
  631. */
  632. public function getChannelAttribute(): ?string
  633. {
  634. return $this->channel;
  635. }
  636. /**
  637. * Get channel context.
  638. */
  639. #[ApiProperty(
  640. writable: true,
  641. readable: true
  642. )]
  643. #[Groups(['mutation'])]
  644. public function getChannel(): ?string
  645. {
  646. return $this->getChannelAttribute();
  647. }
  648. /**
  649. * Set channel context.
  650. */
  651. public function setChannel(?string $value): void
  652. {
  653. $this->channel = $value;
  654. }
  655. /**
  656. * Get super attributes relationship (many-to-many).
  657. */
  658. public function super_attributes(): BelongsToMany
  659. {
  660. return $this->belongsToMany(Attribute::class, 'product_super_attributes');
  661. }
  662. /**
  663. * Get super attributes.
  664. */
  665. #[ApiProperty(
  666. writable: true,
  667. readable: true,
  668. required: false
  669. )]
  670. #[Groups(['mutation'])]
  671. public function getSuper_attributes()
  672. {
  673. return $this->super_attributes;
  674. }
  675. /**
  676. * Set super attributes.
  677. */
  678. public function setSuper_attributes($value): void
  679. {
  680. $this->super_attributes = $value;
  681. }
  682. /**
  683. * Get product categories relationship.
  684. */
  685. public function categories(): BelongsToMany
  686. {
  687. return $this->belongsToMany(Category::class, 'product_categories');
  688. }
  689. /**
  690. * Get product categories.
  691. */
  692. #[ApiProperty(
  693. writable: true,
  694. readable: true,
  695. required: false
  696. )]
  697. #[Groups(['mutation'])]
  698. public function getCategories()
  699. {
  700. return $this->categories;
  701. }
  702. /**
  703. * Set product categories.
  704. */
  705. public function setCategories($value): void
  706. {
  707. $this->categories = $value;
  708. }
  709. /**
  710. * The images that belong to the product.
  711. */
  712. public function images(): HasMany
  713. {
  714. return $this->hasMany(ProductImage::class, 'product_id')
  715. ->orderBy('position');
  716. }
  717. #[ApiProperty(
  718. writable: false,
  719. readable: true,
  720. required: false
  721. )]
  722. #[Groups(['mutation'])]
  723. public function getImages()
  724. {
  725. return $this->images;
  726. }
  727. public function setImages($value): void
  728. {
  729. $this->images = $value;
  730. }
  731. /**
  732. * Get product options relationship.
  733. */
  734. public function options(): BelongsToMany
  735. {
  736. return $this->belongsToMany(ProductOption::class, 'product_product_options', 'product_id', 'product_option_id')
  737. ->withPivot(['position', 'is_required', 'meta'])
  738. ->withTimestamps();
  739. }
  740. /**
  741. * Get product options.
  742. */
  743. #[ApiProperty(
  744. writable: false,
  745. readable: true,
  746. required: false
  747. )]
  748. #[Groups(['mutation'])]
  749. public function getOptions(): ?string
  750. {
  751. $jsonData= $this->getOptionsAttribute();
  752. return $jsonData !== '{}' ? $jsonData: null;
  753. }
  754. public function setOptions($value): void
  755. {
  756. $this->options = $value;
  757. }
  758. public function getOptionsAttribute(): string
  759. {
  760. if ($this->type !== 'flexible_variant') {
  761. return '{}';
  762. }
  763. if (! $this->relationLoaded('options')) {
  764. $this->load('options');
  765. }
  766. if (!$this->relationLoaded('flexible_variants')) {
  767. $this->load('flexible_variants');
  768. }
  769. // NOTE: Use getRelation('options') rather than $this->options — the
  770. // latter would recursively invoke this mutator because Eloquent's
  771. // attribute accessor takes precedence over the relation.
  772. $options = $this->getRelation('options') ?? collect();
  773. if ($options->isEmpty()) {
  774. return '{}';
  775. }
  776. $optionIds = $options->pluck('id')->toArray();
  777. $optionCodeMap = $options->pluck('code', 'id')->toArray();
  778. $index = [];
  779. foreach ($options as $option) {
  780. if (! isset($index[$option->id])) {
  781. $index[$option->id] = [];
  782. }
  783. if (! $option->relationLoaded('values')) {
  784. $option->load('values');
  785. }
  786. foreach ($option->values as $value) {
  787. if (in_array($value->id, $optionIds)) {
  788. $optionCode = $optionCodeMap[$value->id] ?? null;
  789. if ($optionCode) {
  790. $index[$option->id][$optionCode] = $value->value;
  791. }
  792. }
  793. }
  794. }
  795. return json_encode($index);
  796. }
  797. /**
  798. * Full product option relation data serialized inline as a JSON string.
  799. *
  800. * Follows the same pattern as ProductVariant::variant_images / option_values
  801. * — returns JSON so that ApiPlatform does not attempt IRI generation for
  802. * ProductOption / ProductOptionValue (they are not registered as
  803. * ApiResources).
  804. *
  805. * Named getProductOptionsAttribute so it is picked up by ApiPlatform's
  806. * EloquentPropertyNameCollectionMetadataFactory as a virtual attribute:
  807. * snake_case 'product_options', GraphQL field 'productOptions'.
  808. *
  809. * Returns JSON like:
  810. * [
  811. * {
  812. * "id": 1, "label": "Size", "code": "size", "type": "dropdown",
  813. * "position": 0, "is_required": true,
  814. * "values": [
  815. * {"id": 1, "label": "Small", "code": "s", "position": 0},
  816. * ...
  817. * ]
  818. * },
  819. * ...
  820. * ]
  821. * Returns null when the product has no options.
  822. */
  823. public function getProductOptionsAttribute(): ?string
  824. {
  825. if (! $this->relationLoaded('options')) {
  826. $this->load('options.values');
  827. }
  828. // NOTE: Use getRelation('options') — accessing $this->options would
  829. // recursively invoke the getOptionsAttribute mutator (which returns a
  830. // JSON-string variant map, not the relation Collection).
  831. $options = $this->getRelation('options') ?? collect();
  832. if ($options->isNotEmpty() && ! $options->first()->relationLoaded('values')) {
  833. $options->loadMissing('values');
  834. }
  835. $payload = $options
  836. ->map(function ($option) {
  837. return [
  838. 'id' => (int) $option->id,
  839. 'label' => $option->label,
  840. 'code' => $option->code,
  841. 'type' => $option->type,
  842. 'position' => (int) ($option->pivot->position ?? $option->position ?? 0),
  843. 'is_required' => (bool) ($option->pivot->is_required ?? false),
  844. 'meta' => $option->pivot->meta ?? $option->meta ?? null,
  845. 'values' => ($option->values ?? collect())
  846. ->map(function($value) {
  847. $isAvailable = $this->flexible_variants->filter(function ($variant) use ($value) {
  848. return $variant->values->contains('id', $value->id);
  849. })->isNotEmpty();
  850. if($isAvailable) {
  851. return [
  852. 'id' => (int) $value->id,
  853. 'label' => $value->label,
  854. 'code' => $value->code,
  855. 'position' => (int) ($value->position ?? 0),
  856. 'meta' => $value->meta ?? null,
  857. 'is_available' => $isAvailable,
  858. ];
  859. }
  860. return null;
  861. })
  862. ->filter(function($value) {
  863. return $value !== null;
  864. })
  865. ->values()
  866. ->all(),
  867. ];
  868. })
  869. ->values()
  870. ->all();
  871. return empty($payload) ? null : json_encode($payload);
  872. }
  873. /**
  874. * Flexible variants — overrides the parent's flexibleVariants() so that
  875. * BagistoApi's ProductVariant wrapper (which has variant_images / values
  876. * relations) is used instead of the bare Longyi\Core\Models\ProductVariant.
  877. */
  878. public function flexibleVariants(): HasMany
  879. {
  880. return $this->hasMany(ProductVariant::class, 'product_id')
  881. ->orderBy('sort_order');
  882. }
  883. /**
  884. * snake_case alias of flexibleVariants().
  885. *
  886. * ApiPlatform's Laravel PropertyAccessor accesses Eloquent models directly
  887. * via $model->{$snake_case_property} and bypasses Symfony's camelize logic.
  888. * Eloquent's isRelation() uses method_exists() with a literal key match, so
  889. * we need a method actually named 'flexible_variants' for the to-many
  890. * property access to resolve to the relation Collection.
  891. *
  892. * Both methods return the same relation — ApiPlatform's Eloquent metadata
  893. * factory deduplicates them into a single 'flexible_variants' property name.
  894. */
  895. public function flexible_variants(): HasMany // phpcs:ignore
  896. {
  897. return $this->flexibleVariants();
  898. }
  899. /**
  900. * Mirror the camelCase-loaded relation into the snake_case slot so that
  901. * $product->flexible_variants does not trigger a second DB query when
  902. * $with = ['flexibleVariants'] has already eager-loaded the variants.
  903. */
  904. public function getRelationValue($key)
  905. {
  906. if ($key === 'flexible_variants'
  907. && ! $this->relationLoaded('flexible_variants')
  908. && $this->relationLoaded('flexibleVariants')
  909. ) {
  910. $collection = $this->getRelation('flexibleVariants');
  911. $this->setRelation('flexible_variants', $collection);
  912. if ($collection->isNotEmpty() && ! $collection->first()->relationLoaded('variant_images')) {
  913. $collection->loadMissing(['variant_images', 'values.option','price_indices']);
  914. }
  915. return $collection;
  916. }
  917. return parent::getRelationValue($key);
  918. }
  919. /**
  920. * The videos that belong to the product.
  921. */
  922. public function videos(): HasMany
  923. {
  924. return $this->hasMany(ProductVideo::class, 'product_id')
  925. ->orderBy('position');
  926. }
  927. #[ApiProperty(
  928. writable: false,
  929. readable: true,
  930. required: false
  931. )]
  932. #[Groups(['mutation'])]
  933. public function getVideos()
  934. {
  935. return $this->videos;
  936. }
  937. public function setVideos($value): void
  938. {
  939. $this->videos = $value;
  940. }
  941. /**
  942. * The images that belong to the product.
  943. */
  944. public function base_image_url(): HasMany
  945. {
  946. return $this->hasMany(ProductImage::class, 'product_id')
  947. ->orderBy('position');
  948. }
  949. #[ApiProperty(
  950. writable: false,
  951. readable: true,
  952. required: true,
  953. readableLink: true
  954. )]
  955. public function getBase_image_url(): ProductImage
  956. {
  957. return $this->base_image_url;
  958. }
  959. public function channels(): BelongsToMany
  960. {
  961. return $this->belongsToMany(Channel::class, 'product_channels', 'product_id', 'channel_id');
  962. }
  963. #[ApiProperty(
  964. writable: true,
  965. readable: true,
  966. required: false
  967. )]
  968. #[Groups(['mutation'])]
  969. public function getChannels()
  970. {
  971. return $this->channels;
  972. }
  973. public function setChannels($value): void
  974. {
  975. $this->channels = $value;
  976. }
  977. /**
  978. * Get configurable product option index attribute.
  979. *
  980. * For configurable products, returns an index mapping variant IDs to their option values by attribute code.
  981. * Format: JSON string like { "588": { "color": 1, "size": 6 }, "589": { "color": 2, "size": 6 }, ... }
  982. *
  983. * This allows headless developers to identify which variant matches selected options.
  984. * Similar to Shop package's ConfigurableOption helper.
  985. */
  986. public function getIndexAttribute(): string
  987. {
  988. return $this->getCombinationsAttribute();
  989. }
  990. #[ApiProperty(deprecationReason: "Use the VariantAttributeMap property instead",writable: false, readable: true, required: false)]
  991. public function getIndex(): ?string
  992. {
  993. $indexJson = $this->getIndexAttribute();
  994. return $indexJson !== '{}' ? $indexJson : null;
  995. }
  996. public function getCombinationsAttribute(): string
  997. {
  998. if ($this->type !== 'configurable') {
  999. return '{}';
  1000. }
  1001. $index = [];
  1002. if (! $this->relationLoaded('super_attributes')) {
  1003. $this->load('super_attributes');
  1004. }
  1005. if (! $this->relationLoaded('variants')) {
  1006. $this->load([
  1007. 'variants' => function ($query) {
  1008. $query->with(['attribute_values.attribute']);
  1009. }
  1010. ]);
  1011. }
  1012. $superAttributeIds = $this->super_attributes->pluck('id')->toArray();
  1013. $attributeCodeMap = $this->super_attributes->pluck('code', 'id')->toArray();
  1014. if (empty($superAttributeIds)) {
  1015. return '{}';
  1016. }
  1017. foreach ($this->variants as $variant) {
  1018. if (! isset($index[$variant->id])) {
  1019. $index[$variant->id] = [];
  1020. }
  1021. // Load variant's attribute values if needed
  1022. if (! $variant->relationLoaded('attribute_values')) {
  1023. $variant->load('attribute_values.attribute');
  1024. }
  1025. // Get the attribute value for each super attribute
  1026. foreach ($variant->attribute_values as $attrValue) {
  1027. // Only include super attributes (configurable attributes)
  1028. if (in_array($attrValue->attribute_id, $superAttributeIds)) {
  1029. $attributeCode = $attributeCodeMap[$attrValue->attribute_id] ?? null;
  1030. if ($attributeCode) {
  1031. $index[$variant->id][$attributeCode] = $attrValue->value;
  1032. }
  1033. }
  1034. }
  1035. }
  1036. return json_encode($index);
  1037. }
  1038. #[ApiProperty(writable: false, readable: true, required: false)]
  1039. public function getCombinations(): ?string
  1040. {
  1041. $indexJson = $this->getCombinationsAttribute();
  1042. return $indexJson !== '{}' ? $indexJson : null;
  1043. }
  1044. public function getSuperAttributeOptionsAttribute(): string
  1045. {
  1046. if ($this->type !== 'configurable') {
  1047. return '{}';
  1048. }
  1049. // Ensure relations are loaded
  1050. if (! $this->relationLoaded('super_attributes')) {
  1051. $this->load('super_attributes');
  1052. }
  1053. if (! $this->relationLoaded('variants')) {
  1054. $this->load([
  1055. 'variants' => function ($query) {
  1056. $query->with(['attribute_values.attribute.options']);
  1057. }
  1058. ]);
  1059. }
  1060. // Step 1: Collect used option IDs per attribute
  1061. $usedOptions = [];
  1062. foreach ($this->variants as $variant) {
  1063. foreach ($variant->attribute_values as $attrValue) {
  1064. $usedOptions[$attrValue->attribute_id][] = $attrValue->value;
  1065. }
  1066. }
  1067. // Deduplicate
  1068. foreach ($usedOptions as $attrId => $values) {
  1069. $usedOptions[$attrId] = array_unique($values);
  1070. }
  1071. // Step 2: Build response
  1072. $result = [];
  1073. foreach ($this->super_attributes as $attribute) {
  1074. $options = [];
  1075. foreach ($attribute->options as $option) {
  1076. if (in_array($option->id, $usedOptions[$attribute->id] ?? [])) {
  1077. $options[] = [
  1078. 'id' => $option->id,
  1079. 'label' => $option->admin_name,
  1080. ];
  1081. }
  1082. }
  1083. if (! empty($options)) {
  1084. $result[] = [
  1085. 'id' => $attribute->id,
  1086. 'code' => $attribute->code,
  1087. 'label' => $attribute->admin_name,
  1088. 'options' => $options,
  1089. ];
  1090. }
  1091. }
  1092. return json_encode($result);
  1093. }
  1094. #[ApiProperty(writable: false, readable: true, required: false)]
  1095. public function getSuper_attribute_options(): ?string
  1096. {
  1097. $indexJson = $this->getSuperAttributeOptionsAttribute();
  1098. return $indexJson !== '{}' ? $indexJson : null;
  1099. }
  1100. public function getSkuAttribute(): ?string
  1101. {
  1102. return $this->getSystemAttributeValue('sku');
  1103. }
  1104. #[ApiProperty(
  1105. writable: true,
  1106. readable: true
  1107. )]
  1108. #[Groups(['mutation'])]
  1109. public function getSku(): ?string
  1110. {
  1111. return $this->getSkuAttribute();
  1112. }
  1113. public function setSku(?string $value): void
  1114. {
  1115. $this->setSystemAttributeValue('sku', $value);
  1116. }
  1117. #[ApiProperty(
  1118. writable: true,
  1119. readable: true,
  1120. required: true
  1121. )]
  1122. #[Groups(['mutation'])]
  1123. public function getType(): ?string
  1124. {
  1125. return $this->type;
  1126. }
  1127. public function setType(?string $value): void
  1128. {
  1129. $this->type = $value;
  1130. }
  1131. #[ApiProperty(
  1132. writable: true,
  1133. readable: true,
  1134. required: true
  1135. )]
  1136. #[Groups(['mutation'])]
  1137. public function getAttribute_family(): ?AttributeFamily
  1138. {
  1139. return $this->attribute_family;
  1140. }
  1141. public function setAttribute_family(?AttributeFamily $value): void
  1142. {
  1143. $this->attribute_family = $value;
  1144. }
  1145. /**
  1146. * Get attribute family relationship
  1147. * Override to return BagistoApi AttributeFamily model
  1148. */
  1149. public function attribute_family(): BelongsTo
  1150. {
  1151. return $this->belongsTo(AttributeFamily::class, 'attribute_family_id');
  1152. }
  1153. #[ApiProperty(
  1154. writable: true,
  1155. readable: true,
  1156. required: true
  1157. )]
  1158. public function getBookingProductsAttributes()
  1159. {
  1160. return $this->getBookingProducts();
  1161. }
  1162. /**
  1163. * Get booking products.
  1164. */
  1165. public function getBookingProducts()
  1166. {
  1167. return $this->booking_products;
  1168. }
  1169. /**
  1170. * Set booking products.
  1171. */
  1172. public function setBookingProducts($value): void
  1173. {
  1174. $this->booking_products = $value;
  1175. }
  1176. /**
  1177. * Get the booking products relationship.
  1178. */
  1179. public function booking_products(): HasMany
  1180. {
  1181. return $this->hasMany(BookingProduct::class, 'product_id');
  1182. }
  1183. /**
  1184. * Get bundle options.
  1185. */
  1186. #[ApiProperty(
  1187. writable: false,
  1188. readable: true,
  1189. required: false
  1190. )]
  1191. #[Groups(['mutation'])]
  1192. public function getBundleOptions()
  1193. {
  1194. return $this->bundle_options;
  1195. }
  1196. public function setBundleOptions($value): void
  1197. {
  1198. $this->bundle_options = $value;
  1199. }
  1200. /**
  1201. * Get the bundle options relationship.
  1202. */
  1203. public function bundle_options(): HasMany
  1204. {
  1205. return $this->hasMany(ProductBundleOption::class);
  1206. }
  1207. public function grouped_products(): HasMany
  1208. {
  1209. return $this->hasMany(ProductGroupedProduct::class, 'product_id');
  1210. }
  1211. public function downloadable_links(): HasMany
  1212. {
  1213. return $this->hasMany(ProductDownloadableLink::class, 'product_id');
  1214. }
  1215. public function downloadable_samples(): HasMany
  1216. {
  1217. return $this->hasMany(ProductDownloadableSample::class, 'product_id');
  1218. }
  1219. /**
  1220. * Get downloadable samples.
  1221. */
  1222. #[ApiProperty(
  1223. writable: false,
  1224. readable: true,
  1225. required: false
  1226. )]
  1227. #[Groups(['mutation'])]
  1228. public function getDownloadableSamples()
  1229. {
  1230. return $this->downloadable_samples;
  1231. }
  1232. public function setDownloadableSamples($value): void
  1233. {
  1234. $this->downloadable_samples = $value;
  1235. }
  1236. /**
  1237. * The customizable options that belong to the product.
  1238. */
  1239. public function customizable_options(): HasMany
  1240. {
  1241. return $this->hasMany(ProductCustomizableOption::class, 'product_id')
  1242. ->orderBy('sort_order');
  1243. }
  1244. /**
  1245. * Get customizable options.
  1246. */
  1247. #[ApiProperty(
  1248. writable: false,
  1249. readable: true,
  1250. required: false
  1251. )]
  1252. #[Groups(['mutation'])]
  1253. public function getCustomizable_options()
  1254. {
  1255. // Eager load prices to ensure they're properly constrained
  1256. return $this->customizable_options()
  1257. ->with('customizable_option_prices')
  1258. ->get();
  1259. }
  1260. public function setCustomizable_options($value): void
  1261. {
  1262. $this->customizable_options = $value;
  1263. }
  1264. /**
  1265. * Get name attribute.
  1266. */
  1267. public function getNameAttribute(): ?string
  1268. {
  1269. return $this->getSystemAttributeValue('name');
  1270. }
  1271. #[ApiProperty(
  1272. writable: true,
  1273. readable: true,
  1274. required: false
  1275. )]
  1276. public function getName(): ?string
  1277. {
  1278. return $this->getNameAttribute();
  1279. }
  1280. public function setName(?string $value): void
  1281. {
  1282. $this->setSystemAttributeValue('name', $value);
  1283. }
  1284. // ========================================
  1285. // URL Key (text, per locale) - Only for update
  1286. // ========================================
  1287. public function getUrlKeyAttribute(): ?string
  1288. {
  1289. return $this->getSystemAttributeValue('url_key');
  1290. }
  1291. #[ApiProperty(
  1292. writable: true,
  1293. readable: true,
  1294. required: false
  1295. )]
  1296. public function getUrl_key(): ?string
  1297. {
  1298. return $this->getUrlKeyAttribute();
  1299. }
  1300. public function setUrl_key(?string $value): void
  1301. {
  1302. $this->setSystemAttributeValue('url_key', $value);
  1303. }
  1304. // ========================================
  1305. // Status (boolean, per channel)
  1306. // ========================================
  1307. public function getStatusAttribute(): ?bool
  1308. {
  1309. return $this->getSystemAttributeValue('status');
  1310. }
  1311. #[ApiProperty(writable: true, readable: true)]
  1312. public function getStatus(): ?bool
  1313. {
  1314. return $this->getStatusAttribute();
  1315. }
  1316. public function setStatus(?bool $value): void
  1317. {
  1318. $this->setSystemAttributeValue('status', $value);
  1319. }
  1320. // ========================================
  1321. // Description (textarea, per locale)
  1322. // ========================================
  1323. public function getDescriptionAttribute(): ?string
  1324. {
  1325. return $this->getSystemAttributeValue('description');
  1326. }
  1327. #[ApiProperty(writable: true, readable: true, required: false)]
  1328. public function getDescription(): ?string
  1329. {
  1330. return $this->getDescriptionAttribute();
  1331. }
  1332. public function setDescription(?string $value): void
  1333. {
  1334. $this->setSystemAttributeValue('description', $value);
  1335. }
  1336. /**
  1337. * Laravel accessor for description_html appended attribute
  1338. */
  1339. public function getDescriptionHtmlAttribute(): ?string
  1340. {
  1341. return $this->getDescriptionAttribute();
  1342. }
  1343. #[ApiProperty(writable: false, readable: true, required: false)]
  1344. public function getDescription_html(): ?string
  1345. {
  1346. return $this->getDescription_htmlAttribute();
  1347. }
  1348. public function getShortDescriptionAttribute(): ?string
  1349. {
  1350. return $this->getSystemAttributeValue('short_description');
  1351. }
  1352. #[ApiProperty(
  1353. writable: true,
  1354. readable: true,
  1355. required: false,
  1356. schema: ['type' => 'string', 'nullable' => true],
  1357. openapiContext: ['nullable' => true],
  1358. jsonSchemaContext: ['type' => 'string', 'nullable' => true]
  1359. )]
  1360. public function getShort_description(): ?string
  1361. {
  1362. return $this->getShort_descriptionAttribute();
  1363. }
  1364. public function setShort_description(?string $value): void
  1365. {
  1366. $this->setSystemAttributeValue('short_description', $value);
  1367. }
  1368. public function getPriceAttribute(): ?float
  1369. {
  1370. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price')));
  1371. }
  1372. #[ApiProperty(writable: true, readable: true)]
  1373. public function getPrice(): ?float
  1374. {
  1375. return $this->getPriceAttribute();
  1376. }
  1377. public function setPrice(?float $value): void
  1378. {
  1379. $this->setSystemAttributeValue('price', $value);
  1380. }
  1381. public function getSpecialPriceAttribute(): ?float
  1382. {
  1383. $value = floatval($this->getSystemAttributeValue('special_price'));
  1384. return $value ? (float) core()->convertPrice($value) : null;
  1385. }
  1386. #[ApiProperty(writable: true, readable: true)]
  1387. public function getSpecial_price(): ?float
  1388. {
  1389. return $this->getSpecialPriceAttribute();
  1390. }
  1391. public function setSpecial_price(?float $value): void
  1392. {
  1393. $this->setSystemAttributeValue('special_price', $value);
  1394. }
  1395. public function getWeightAttribute(): ?string
  1396. {
  1397. return $this->getSystemAttributeValue('weight');
  1398. }
  1399. #[ApiProperty(writable: true, readable: true)]
  1400. public function getWeight(): ?string
  1401. {
  1402. return $this->getWeightAttribute();
  1403. }
  1404. public function setWeight(?string $value): void
  1405. {
  1406. $this->setSystemAttributeValue('weight', $value);
  1407. }
  1408. public function getProductNumberAttribute(): ?string
  1409. {
  1410. return $this->getSystemAttributeValue('product_number');
  1411. }
  1412. #[ApiProperty(writable: true, readable: true)]
  1413. public function getProduct_number(): ?string
  1414. {
  1415. return $this->getProductNumberAttribute();
  1416. }
  1417. public function setProduct_number(?string $value): void
  1418. {
  1419. $this->setSystemAttributeValue('product_number', $value);
  1420. }
  1421. public function getNewAttribute(): ?bool
  1422. {
  1423. return $this->getSystemAttributeValue('new');
  1424. }
  1425. #[ApiProperty(
  1426. writable: true,
  1427. readable: true,
  1428. required: false,
  1429. schema: ['type' => 'boolean', 'nullable' => true],
  1430. )]
  1431. public function getNew(): bool
  1432. {
  1433. return $this->getNewAttribute();
  1434. }
  1435. public function setNew(bool $value): void
  1436. {
  1437. $this->setSystemAttributeValue('new', $value);
  1438. }
  1439. public function getFeaturedAttribute(): ?bool
  1440. {
  1441. return $this->getSystemAttributeValue('featured');
  1442. }
  1443. #[ApiProperty(writable: true, readable: true)]
  1444. public function getFeatured(): ?bool
  1445. {
  1446. return $this->getFeaturedAttribute();
  1447. }
  1448. public function setFeatured(?bool $value): void
  1449. {
  1450. $this->setSystemAttributeValue('featured', $value);
  1451. }
  1452. // ========================================
  1453. // Visible Individually (boolean)
  1454. // ========================================
  1455. public function getVisibleIndividuallyAttribute(): ?bool
  1456. {
  1457. return $this->getSystemAttributeValue('visible_individually');
  1458. }
  1459. #[ApiProperty(writable: true, readable: true)]
  1460. public function getVisible_individually(): ?bool
  1461. {
  1462. return $this->getVisibleIndividuallyAttribute();
  1463. }
  1464. public function setVisible_individually(?bool $value): void
  1465. {
  1466. $this->setSystemAttributeValue('visible_individually', $value);
  1467. }
  1468. // ========================================
  1469. // Guest Checkout (boolean)
  1470. // ========================================
  1471. public function getGuestCheckoutAttribute(): ?bool
  1472. {
  1473. return $this->getSystemAttributeValue('guest_checkout');
  1474. }
  1475. #[ApiProperty(writable: true, readable: true)]
  1476. public function getGuest_checkout(): ?bool
  1477. {
  1478. return $this->getGuestCheckoutAttribute();
  1479. }
  1480. public function setGuest_checkout(?bool $value): void
  1481. {
  1482. $this->setSystemAttributeValue('guest_checkout', $value);
  1483. }
  1484. // ========================================
  1485. // Manage Stock (boolean, per channel)
  1486. // ========================================
  1487. public function getManageStockAttribute(): ?bool
  1488. {
  1489. return $this->getSystemAttributeValue('manage_stock');
  1490. }
  1491. #[ApiProperty(writable: true, readable: true)]
  1492. public function getManage_stock(): ?bool
  1493. {
  1494. return $this->getManageStockAttribute();
  1495. }
  1496. public function setManage_stock(?bool $value): void
  1497. {
  1498. $this->setSystemAttributeValue('manage_stock', $value);
  1499. }
  1500. // ========================================
  1501. // Meta Title (textarea, per locale)
  1502. // ========================================
  1503. public function getMetaTitleAttribute(): ?string
  1504. {
  1505. return $this->getSystemAttributeValue('meta_title');
  1506. }
  1507. #[ApiProperty(writable: true, readable: true)]
  1508. public function getMeta_title(): ?string
  1509. {
  1510. return $this->getMetaTitleAttribute();
  1511. }
  1512. public function setMeta_title(?string $value): void
  1513. {
  1514. $this->setSystemAttributeValue('meta_title', $value);
  1515. }
  1516. // ========================================
  1517. // Meta Keywords (textarea, per locale)
  1518. // ========================================
  1519. public function getMetaKeywordsAttribute(): ?string
  1520. {
  1521. return $this->getSystemAttributeValue('meta_keywords');
  1522. }
  1523. #[ApiProperty(writable: true, readable: true)]
  1524. public function getMeta_keywords(): ?string
  1525. {
  1526. return $this->getMetaKeywordsAttribute();
  1527. }
  1528. public function setMeta_keywords(?string $value): void
  1529. {
  1530. $this->setSystemAttributeValue('meta_keywords', $value);
  1531. }
  1532. // ========================================
  1533. // Tax Category ID (select, per channel)
  1534. // ========================================
  1535. public function getTaxCategoryIdAttribute(): ?int
  1536. {
  1537. return (int) $this->getSystemAttributeValue('tax_category_id');
  1538. }
  1539. #[ApiProperty(writable: true, readable: true)]
  1540. public function getTax_category_id(): ?int
  1541. {
  1542. return $this->getTaxCategoryIdAttribute();
  1543. }
  1544. public function setTax_category_id(?int $value): void
  1545. {
  1546. $this->setSystemAttributeValue('tax_category_id', $value);
  1547. }
  1548. // ========================================
  1549. // Special Price From (date, per channel)
  1550. // ========================================
  1551. public function getSpecialPriceFromAttribute(): ?string
  1552. {
  1553. return $this->getSystemAttributeValue('special_price_from');
  1554. }
  1555. #[ApiProperty(writable: true, readable: true)]
  1556. public function getSpecial_price_from(): ?string
  1557. {
  1558. return $this->getSpecialPriceFromAttribute();
  1559. }
  1560. public function setSpecial_price_from(?string $value): void
  1561. {
  1562. $this->setSystemAttributeValue('special_price_from', $value);
  1563. }
  1564. // ========================================
  1565. // Special Price To (date, per channel)
  1566. // ========================================
  1567. public function getSpecialPriceToAttribute(): ?string
  1568. {
  1569. return $this->getSystemAttributeValue('special_price_to');
  1570. }
  1571. #[ApiProperty(writable: true, readable: true)]
  1572. public function getSpecial_price_to(): ?string
  1573. {
  1574. return $this->getSpecialPriceToAttribute();
  1575. }
  1576. public function setSpecial_price_to(?string $value): void
  1577. {
  1578. $this->setSystemAttributeValue('special_price_to', $value);
  1579. }
  1580. // ========================================
  1581. // Cost (price) - User-defined
  1582. // ========================================
  1583. public function getCostAttribute()
  1584. {
  1585. return floatval($this->getSystemAttributeValue('cost'));
  1586. }
  1587. #[ApiProperty(writable: true, readable: true)]
  1588. public function getCost(): ?float
  1589. {
  1590. return $this->getCostAttribute();
  1591. }
  1592. public function setCost(?float $value): void
  1593. {
  1594. $this->setSystemAttributeValue('cost', $value);
  1595. }
  1596. // ========================================
  1597. // Meta Description (textarea, per locale) - User-defined
  1598. // ========================================
  1599. public function getMetaDescriptionAttribute(): ?string
  1600. {
  1601. return $this->getSystemAttributeValue('meta_description');
  1602. }
  1603. #[ApiProperty(writable: true, readable: true)]
  1604. public function getMeta_description(): ?string
  1605. {
  1606. return $this->getMetaDescriptionAttribute();
  1607. }
  1608. public function setMeta_description(?string $value): void
  1609. {
  1610. $this->setSystemAttributeValue('meta_description', $value);
  1611. }
  1612. // ========================================
  1613. // Length (text) - User-defined
  1614. // ========================================
  1615. public function getLengthAttribute(): ?string
  1616. {
  1617. return $this->getSystemAttributeValue('length');
  1618. }
  1619. #[ApiProperty(writable: true, readable: true)]
  1620. public function getLength(): ?string
  1621. {
  1622. return $this->getLengthAttribute();
  1623. }
  1624. public function setLength(?string $value): void
  1625. {
  1626. $this->setSystemAttributeValue('length', $value);
  1627. }
  1628. // ========================================
  1629. // Width (text) - User-defined
  1630. // ========================================
  1631. public function getWidthAttribute(): ?string
  1632. {
  1633. return $this->getSystemAttributeValue('width');
  1634. }
  1635. #[ApiProperty(writable: true, readable: true)]
  1636. public function getWidth(): ?string
  1637. {
  1638. return $this->getWidthAttribute();
  1639. }
  1640. public function setWidth(?string $value): void
  1641. {
  1642. $this->setSystemAttributeValue('width', $value);
  1643. }
  1644. // ========================================
  1645. // Height (text) - User-defined
  1646. // ========================================
  1647. public function getHeightAttribute(): ?string
  1648. {
  1649. return $this->getSystemAttributeValue('height');
  1650. }
  1651. #[ApiProperty(writable: true, readable: true)]
  1652. public function getHeight(): ?string
  1653. {
  1654. return $this->getHeightAttribute();
  1655. }
  1656. public function setHeight(?string $value): void
  1657. {
  1658. $this->setSystemAttributeValue('height', $value);
  1659. }
  1660. // ========================================
  1661. // Color (select) - User-defined
  1662. // ========================================
  1663. public function getColorAttribute()
  1664. {
  1665. return $this->getSystemAttributeValue('color');
  1666. }
  1667. #[ApiProperty(writable: true, readable: true)]
  1668. public function getColor(): ?int
  1669. {
  1670. return $this->getColorAttribute();
  1671. }
  1672. public function setColor(?int $value): void
  1673. {
  1674. $this->setSystemAttributeValue('color', $value);
  1675. }
  1676. // ========================================
  1677. // Size (select) - User-defined
  1678. // ========================================
  1679. public function getSizeAttribute()
  1680. {
  1681. $sizeValue = $this->getSystemAttributeValue('size');
  1682. return is_array($sizeValue) ? implode(',', $sizeValue) : $sizeValue;
  1683. }
  1684. #[ApiProperty(writable: true, readable: true)]
  1685. public function getSize(): ?int
  1686. {
  1687. return $this->getSizeAttribute();
  1688. }
  1689. public function setSize(?int $value): void
  1690. {
  1691. $this->setSystemAttributeValue('size', $value);
  1692. }
  1693. // ========================================
  1694. // Brand (select) - User-defined
  1695. // ========================================
  1696. public function getBrandAttribute()
  1697. {
  1698. return $this->getSystemAttributeValue('brand');
  1699. }
  1700. #[ApiProperty(writable: true, readable: true)]
  1701. public function getBrand(): ?int
  1702. {
  1703. return $this->getBrandAttribute();
  1704. }
  1705. public function setBrand(?int $value): void
  1706. {
  1707. $this->setSystemAttributeValue('brand', $value);
  1708. }
  1709. #[ApiProperty(writable: false, readable: true, required: false)]
  1710. public function getReviews()
  1711. {
  1712. return function ($source, array $args = [], $context = null) {
  1713. $relation = $this->reviews();
  1714. /** Only return approved reviews unless a specific status is requested */
  1715. $relation = $relation->where('status', $args['status'] ?? 'approved');
  1716. if (isset($args['first']) && is_numeric($args['first'])) {
  1717. $relation = $relation->limit((int) $args['first']);
  1718. }
  1719. if (empty($args) && $this->relationLoaded('reviews')) {
  1720. return $this->reviews->where('status', 'approved')->values();
  1721. }
  1722. return $relation->get();
  1723. };
  1724. }
  1725. /**
  1726. * Get the product reviews that owns the product.
  1727. */
  1728. public function reviews(): HasMany
  1729. {
  1730. return $this->hasMany(ProductReview::class);
  1731. }
  1732. // ========================================
  1733. // Helper Methods
  1734. // ========================================
  1735. /**
  1736. * Cache for attribute values to avoid repeated lookups
  1737. */
  1738. protected array $attributeValueCache = [];
  1739. /**
  1740. * Get a system attribute value from product_attribute_values
  1741. * This reads from the database when querying
  1742. * OPTIMIZED: Uses memoization to cache attribute values within the same request
  1743. */
  1744. protected function getSystemAttributeValue(string $attributeCode): mixed
  1745. {
  1746. // Check cache first
  1747. if (array_key_exists($attributeCode, $this->attributeValueCache)) {
  1748. return $this->attributeValueCache[$attributeCode];
  1749. }
  1750. // If value was set via setter (during input), return it from temporary storage
  1751. $tempKey = "_temp_{$attributeCode}";
  1752. if (isset($this->attributes[$tempKey])) {
  1753. return $this->attributeValueCache[$attributeCode] = $this->attributes[$tempKey];
  1754. }
  1755. // Otherwise, read from database via relationship
  1756. if (! $this->relationLoaded('attribute_values')) {
  1757. $this->load('attribute_values');
  1758. }
  1759. $attrConfig = static::$systemAttributes[$attributeCode] ?? null;
  1760. if (! $attrConfig) {
  1761. return $this->attributeValueCache[$attributeCode] = '';
  1762. }
  1763. $currentLocale = $this->locale ?? app()->getLocale();
  1764. $currentChannel = $this->channel ?? (core()->getCurrentChannel()->code ?? 'default');
  1765. $attributeValue = null;
  1766. $localeVariants = [];
  1767. if (! empty($currentLocale)) {
  1768. $localeVariants[] = $currentLocale;
  1769. if (str_contains($currentLocale, '_') || str_contains($currentLocale, '-')) {
  1770. $localeParts = preg_split('/[_-]/', $currentLocale);
  1771. if (! empty($localeParts[0])) {
  1772. $localeVariants[] = $localeParts[0];
  1773. }
  1774. }
  1775. }
  1776. // Fallback to the channel's default locale when the requested locale has no translation
  1777. $defaultLocale = core()->getCurrentChannel()->default_locale?->code;
  1778. if ($defaultLocale && ! in_array($defaultLocale, $localeVariants)) {
  1779. $localeVariants[] = $defaultLocale;
  1780. }
  1781. $localeVariants[] = null;
  1782. $channelVariants = [$currentChannel, null];
  1783. foreach ($localeVariants as $localeVariant) {
  1784. foreach ($channelVariants as $channelVariant) {
  1785. $query = $this->attribute_values->where('attribute_id', $attrConfig['id']);
  1786. if ($localeVariant === null) {
  1787. $query = $query->whereNull('locale');
  1788. } else {
  1789. $query = $query->where('locale', $localeVariant);
  1790. }
  1791. if ($channelVariant === null) {
  1792. $query = $query->whereNull('channel');
  1793. } else {
  1794. $query = $query->where('channel', $channelVariant);
  1795. }
  1796. $attributeValue = $query->first();
  1797. if ($attributeValue) {
  1798. break 2;
  1799. }
  1800. }
  1801. }
  1802. if ($attributeValue && $attributeValue?->integer_value && in_array($attributeValue?->attribute?->type, ['select', 'multiselect', 'checkbox'])) {
  1803. $attributeValue->setValue($attributeValue->attribute->options()->where('id', $attributeValue->value)->first()?->label);
  1804. }
  1805. return $this->attributeValueCache[$attributeCode] = ($attributeValue ? $attributeValue->value : '');
  1806. }
  1807. /**
  1808. * Set a system attribute value (will be processed by ProductProcessor)
  1809. * This stores in temporary attributes array for processing later
  1810. */
  1811. protected function setSystemAttributeValue(string $attributeCode, mixed $value): void
  1812. {
  1813. $tempKey = "_temp_{$attributeCode}";
  1814. $this->attributes[$tempKey] = $value;
  1815. }
  1816. public function related_products(): BelongsToMany
  1817. {
  1818. return $this->belongsToMany(static::class, 'product_relations', 'parent_id', 'child_id');
  1819. }
  1820. #[ApiProperty(writable: true, readable: true, required: false)]
  1821. public function getRelatedProducts()
  1822. {
  1823. return function ($source, array $args = [], $context = null) {
  1824. $relation = $source->related_products();
  1825. $total = $relation->count();
  1826. $limit = $args['first'] ?? $args['last'] ?? 30;
  1827. $items = $relation->limit($limit)->get();
  1828. return new \Illuminate\Pagination\LengthAwarePaginator(
  1829. $items,
  1830. $total,
  1831. $limit,
  1832. 1,
  1833. ['path' => '/']
  1834. );
  1835. };
  1836. }
  1837. public function up_sells(): BelongsToMany
  1838. {
  1839. return $this->belongsToMany(static::class, 'product_up_sells', 'parent_id', 'child_id');
  1840. }
  1841. #[ApiProperty(writable: true, readable: true, required: false)]
  1842. public function getUpSells()
  1843. {
  1844. // Return a Closure so ResourceFieldResolver invokes it with ($source, $args, $context)
  1845. return function ($source, array $args = [], $context = null) {
  1846. $relation = $source->up_sells();
  1847. // Get total count before applying limit
  1848. $total = $relation->count();
  1849. // Apply first/last pagination if provided
  1850. $limit = $args['first'] ?? $args['last'] ?? 30;
  1851. $items = $relation->limit($limit)->get();
  1852. // Return a LengthAwarePaginator so ApiPlatform can compute totalCount
  1853. return new \Illuminate\Pagination\LengthAwarePaginator(
  1854. $items,
  1855. $total,
  1856. $limit,
  1857. 1,
  1858. ['path' => '/']
  1859. );
  1860. };
  1861. }
  1862. public function cross_sells(): BelongsToMany
  1863. {
  1864. return $this->belongsToMany(static::class, 'product_cross_sells', 'parent_id', 'child_id');
  1865. }
  1866. #[ApiProperty(writable: true, readable: true, required: false)]
  1867. public function getCrossSells()
  1868. {
  1869. // Return a Closure so ResourceFieldResolver invokes it with ($source, $args, $context)
  1870. return function ($source, array $args = [], $context = null) {
  1871. $relation = $source->cross_sells();
  1872. // Get total count before applying limit
  1873. $total = $relation->count();
  1874. // Apply first/last pagination if provided
  1875. $limit = $args['first'] ?? $args['last'] ?? 30;
  1876. $items = $relation->limit($limit)->get();
  1877. // Return a LengthAwarePaginator so ApiPlatform can compute totalCount
  1878. return new \Illuminate\Pagination\LengthAwarePaginator(
  1879. $items,
  1880. $total,
  1881. $limit,
  1882. 1,
  1883. ['path' => '/']
  1884. );
  1885. };
  1886. }
  1887. #[ApiProperty(writable: false, readable: true, required: false)]
  1888. public function getProductPrices()
  1889. {
  1890. return $this->product_prices;
  1891. }
  1892. // ========================================
  1893. // Minimum and Maximum Price (computed)
  1894. // ========================================
  1895. /**
  1896. * Laravel accessor for minimum_price attribute.
  1897. * Get product minimum price based on price index.
  1898. * Falls back to base price if no price index is available.
  1899. */
  1900. public function getMinimumPriceAttribute(): float
  1901. {
  1902. try {
  1903. // Load price indices if not already loaded
  1904. if (! $this->relationLoaded('price_indices')) {
  1905. $this->load('price_indices');
  1906. }
  1907. // Get current channel and customer group
  1908. $currentChannel = core()->getCurrentChannel();
  1909. $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
  1910. if (! $currentChannel || ! $customerGroup) {
  1911. return floatval($this->price ?? 0);
  1912. }
  1913. // Get price index for current channel and customer group
  1914. $priceIndex = $this->price_indices
  1915. ->where('channel_id', $currentChannel->id)
  1916. ->where('customer_group_id', $customerGroup->id)
  1917. ->first();
  1918. if ($priceIndex) {
  1919. return (float) core()->convertPrice(floatval($priceIndex->min_price));
  1920. }
  1921. // Fallback to base price
  1922. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  1923. } catch (\Exception $e) {
  1924. // If any error occurs, return base price
  1925. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  1926. }
  1927. }
  1928. /**
  1929. * Get product minimum price for BagistoApi API.
  1930. * Exposed to BagistoApi schema via ApiProperty attribute.
  1931. */
  1932. #[ApiProperty(writable: false, readable: true, required: false)]
  1933. public function getMinimum_price(): float
  1934. {
  1935. return $this->getMinimumPriceAttribute();
  1936. }
  1937. /**
  1938. * Laravel accessor for maximum_price attribute.
  1939. * Get product maximum price based on price index.
  1940. * Falls back to base price if no price index is available.
  1941. */
  1942. public function getMaximumPriceAttribute(): float
  1943. {
  1944. try {
  1945. // Load price indices if not already loaded
  1946. if (! $this->relationLoaded('price_indices')) {
  1947. $this->load('price_indices');
  1948. }
  1949. // Get current channel and customer group
  1950. $currentChannel = core()->getCurrentChannel();
  1951. $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
  1952. if (! $currentChannel || ! $customerGroup) {
  1953. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  1954. }
  1955. // Get price index for current channel and customer group
  1956. $priceIndex = $this->price_indices
  1957. ->where('channel_id', $currentChannel->id)
  1958. ->where('customer_group_id', $customerGroup->id)
  1959. ->first();
  1960. if ($priceIndex) {
  1961. return (float) core()->convertPrice(floatval($priceIndex->max_price));
  1962. }
  1963. // Fallback to base price
  1964. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  1965. } catch (\Exception $e) {
  1966. // If any error occurs, return base price
  1967. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  1968. }
  1969. }
  1970. /**
  1971. * Get product maximum price for BagistoApi API.
  1972. * Exposed to BagistoApi schema via ApiProperty attribute.
  1973. */
  1974. #[ApiProperty(writable: false, readable: true, required: false)]
  1975. public function getMaximum_price(): float
  1976. {
  1977. return $this->getMaximumPriceAttribute();
  1978. }
  1979. /**
  1980. * Laravel accessor for regular_minimum_price attribute.
  1981. * Get product regular minimum price based on price index.
  1982. * Falls back to base price if no price index is available.
  1983. */
  1984. public function getRegularMinimumPriceAttribute(): float
  1985. {
  1986. try {
  1987. // Load price indices if not already loaded
  1988. if (! $this->relationLoaded('price_indices')) {
  1989. $this->load('price_indices');
  1990. }
  1991. // Get current channel and customer group
  1992. $currentChannel = core()->getCurrentChannel();
  1993. $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
  1994. if (! $currentChannel || ! $customerGroup) {
  1995. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  1996. }
  1997. // Get price index for current channel and customer group
  1998. $priceIndex = $this->price_indices
  1999. ->where('channel_id', $currentChannel->id)
  2000. ->where('customer_group_id', $customerGroup->id)
  2001. ->first();
  2002. if ($priceIndex) {
  2003. return (float) core()->convertPrice(floatval($priceIndex->regular_min_price));
  2004. }
  2005. // Fallback to base price
  2006. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  2007. } catch (\Exception $e) {
  2008. // If any error occurs, return base price
  2009. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  2010. }
  2011. }
  2012. /**
  2013. * Get product regular minimum price for BagistoApi API.
  2014. * Exposed to BagistoApi schema via ApiProperty attribute.
  2015. */
  2016. #[ApiProperty(writable: false, readable: true, required: false)]
  2017. public function getRegular_minimum_price(): float
  2018. {
  2019. return $this->getRegularMinimumPriceAttribute();
  2020. }
  2021. /**
  2022. * Laravel accessor for regular_maximum_price attribute.
  2023. * Get product regular maximum price based on price index.
  2024. * Falls back to base price if no price index is available.
  2025. */
  2026. public function getRegularMaximumPriceAttribute(): float
  2027. {
  2028. try {
  2029. // Load price indices if not already loaded
  2030. if (! $this->relationLoaded('price_indices')) {
  2031. $this->load('price_indices');
  2032. }
  2033. // Get current channel and customer group
  2034. $currentChannel = core()->getCurrentChannel();
  2035. $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
  2036. if (! $currentChannel || ! $customerGroup) {
  2037. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  2038. }
  2039. // Get price index for current channel and customer group
  2040. $priceIndex = $this->price_indices
  2041. ->where('channel_id', $currentChannel->id)
  2042. ->where('customer_group_id', $customerGroup->id)
  2043. ->first();
  2044. if ($priceIndex) {
  2045. return (float) core()->convertPrice(floatval($priceIndex->regular_max_price));
  2046. }
  2047. // Fallback to base price
  2048. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  2049. } catch (\Exception $e) {
  2050. // If any error occurs, return base price
  2051. return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
  2052. }
  2053. }
  2054. /**
  2055. * Get product regular maximum price for BagistoApi API.
  2056. * Exposed to BagistoApi schema via ApiProperty attribute.
  2057. */
  2058. #[ApiProperty(writable: false, readable: true, required: false)]
  2059. public function getRegular_maximum_price(): float
  2060. {
  2061. return $this->getRegularMaximumPriceAttribute();
  2062. }
  2063. // ─── Formatted Price Accessors ──────────────────────────────────────
  2064. public function getFormattedPriceAttribute(): ?string
  2065. {
  2066. $price = $this->getPriceAttribute();
  2067. return $price !== null ? core()->formatPrice($price) : null;
  2068. }
  2069. #[ApiProperty(writable: false, readable: true, required: false)]
  2070. public function getFormatted_price(): ?string
  2071. {
  2072. return $this->getFormattedPriceAttribute();
  2073. }
  2074. public function getFormattedSpecialPriceAttribute(): ?string
  2075. {
  2076. $specialPrice = $this->getSpecialPriceAttribute();
  2077. return $specialPrice ? core()->formatPrice($specialPrice) : null;
  2078. }
  2079. #[ApiProperty(writable: false, readable: true, required: false)]
  2080. public function getFormatted_special_price(): ?string
  2081. {
  2082. return $this->getFormattedSpecialPriceAttribute();
  2083. }
  2084. public function getFormattedMinimumPriceAttribute(): ?string
  2085. {
  2086. return core()->formatPrice($this->getMinimumPriceAttribute());
  2087. }
  2088. #[ApiProperty(writable: false, readable: true, required: false)]
  2089. public function getFormatted_minimum_price(): ?string
  2090. {
  2091. return $this->getFormattedMinimumPriceAttribute();
  2092. }
  2093. public function getFormattedMaximumPriceAttribute(): ?string
  2094. {
  2095. return core()->formatPrice($this->getMaximumPriceAttribute());
  2096. }
  2097. #[ApiProperty(writable: false, readable: true, required: false)]
  2098. public function getFormatted_maximum_price(): ?string
  2099. {
  2100. return $this->getFormattedMaximumPriceAttribute();
  2101. }
  2102. public function getFormattedRegularMinimumPriceAttribute(): ?string
  2103. {
  2104. return core()->formatPrice($this->getRegularMinimumPriceAttribute());
  2105. }
  2106. #[ApiProperty(writable: false, readable: true, required: false)]
  2107. public function getFormatted_regular_minimum_price(): ?string
  2108. {
  2109. return $this->getFormattedRegularMinimumPriceAttribute();
  2110. }
  2111. public function getFormattedRegularMaximumPriceAttribute(): ?string
  2112. {
  2113. return core()->formatPrice($this->getRegularMaximumPriceAttribute());
  2114. }
  2115. #[ApiProperty(writable: false, readable: true, required: false)]
  2116. public function getFormatted_regular_maximum_price(): ?string
  2117. {
  2118. return $this->getFormattedRegularMaximumPriceAttribute();
  2119. }
  2120. }