@fleetbase/registry-bridge-engine 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/.php-cs-fixer.php +29 -0
  2. package/LICENSE.md +651 -0
  3. package/README.md +122 -0
  4. package/addon/adapters/registry-bridge.js +5 -0
  5. package/addon/adapters/registry-extension-bundle.js +1 -0
  6. package/addon/adapters/registry-extension.js +1 -0
  7. package/addon/components/extension-card.hbs +12 -0
  8. package/addon/components/extension-card.js +235 -0
  9. package/addon/components/extension-form.hbs +237 -0
  10. package/addon/components/extension-form.js +123 -0
  11. package/addon/components/extension-modal-title.hbs +14 -0
  12. package/addon/components/extension-modal-title.js +20 -0
  13. package/addon/components/extension-monetize-form.hbs +56 -0
  14. package/addon/components/extension-monetize-form.js +7 -0
  15. package/addon/components/extension-pending-publish-viewer.hbs +52 -0
  16. package/addon/components/extension-pending-publish-viewer.js +37 -0
  17. package/addon/components/extension-reviewer-control.hbs +68 -0
  18. package/addon/components/extension-reviewer-control.js +68 -0
  19. package/addon/components/modals/confirm-extension-purchase.hbs +5 -0
  20. package/addon/components/modals/confirm-extension-purchase.js +3 -0
  21. package/addon/components/modals/extension-details.hbs +69 -0
  22. package/addon/components/modals/extension-details.js +33 -0
  23. package/addon/components/modals/extension-purchase-form.hbs +5 -0
  24. package/addon/components/modals/extension-purchase-form.js +3 -0
  25. package/addon/components/modals/extension-uninstall.hbs +25 -0
  26. package/addon/components/modals/extension-uninstall.js +11 -0
  27. package/addon/components/modals/select-extension-bundle.hbs +43 -0
  28. package/addon/components/modals/select-extension-bundle.js +31 -0
  29. package/addon/components/progress-bar.hbs +12 -0
  30. package/addon/components/progress-bar.js +8 -0
  31. package/addon/controllers/application.js +6 -0
  32. package/addon/controllers/developers/analytics.js +26 -0
  33. package/addon/controllers/developers/extensions/edit/bundles.js +70 -0
  34. package/addon/controllers/developers/extensions/edit/index.js +3 -0
  35. package/addon/controllers/developers/extensions/edit/monetize.js +7 -0
  36. package/addon/controllers/developers/extensions/edit.js +107 -0
  37. package/addon/controllers/developers/extensions/index.js +3 -0
  38. package/addon/controllers/developers/extensions/new.js +32 -0
  39. package/addon/controllers/developers/payments/index.js +39 -0
  40. package/addon/controllers/developers/payments/onboard.js +67 -0
  41. package/addon/controllers/explore/category.js +22 -0
  42. package/addon/controllers/explore/index.js +15 -0
  43. package/addon/controllers/installed.js +86 -0
  44. package/addon/controllers/purchased.js +18 -0
  45. package/addon/engine.js +44 -0
  46. package/addon/models/registry-extension-bundle.js +62 -0
  47. package/addon/models/registry-extension.js +215 -0
  48. package/addon/routes/application.js +12 -0
  49. package/addon/routes/developers/analytics.js +10 -0
  50. package/addon/routes/developers/credentials.js +3 -0
  51. package/addon/routes/developers/extensions/edit/bundles.js +21 -0
  52. package/addon/routes/developers/extensions/edit/details.js +3 -0
  53. package/addon/routes/developers/extensions/edit/index.js +3 -0
  54. package/addon/routes/developers/extensions/edit/monetize.js +3 -0
  55. package/addon/routes/developers/extensions/edit.js +18 -0
  56. package/addon/routes/developers/extensions/index.js +10 -0
  57. package/addon/routes/developers/extensions/new.js +3 -0
  58. package/addon/routes/developers/extensions.js +3 -0
  59. package/addon/routes/developers/payments/index.js +26 -0
  60. package/addon/routes/developers/payments/onboard.js +21 -0
  61. package/addon/routes/developers/payments.js +3 -0
  62. package/addon/routes/developers.js +3 -0
  63. package/addon/routes/explore/category.js +27 -0
  64. package/addon/routes/explore/index.js +17 -0
  65. package/addon/routes/explore.js +3 -0
  66. package/addon/routes/installed.js +10 -0
  67. package/addon/routes/purchased.js +10 -0
  68. package/addon/routes.js +28 -0
  69. package/addon/serializers/registry-extension-bundle.js +15 -0
  70. package/addon/serializers/registry-extension.js +21 -0
  71. package/addon/services/stripe.js +83 -0
  72. package/addon/styles/registry-bridge-engine.css +142 -0
  73. package/addon/templates/application.hbs +26 -0
  74. package/addon/templates/developers/analytics.hbs +83 -0
  75. package/addon/templates/developers/credentials.hbs +1 -0
  76. package/addon/templates/developers/extensions/edit/bundles.hbs +71 -0
  77. package/addon/templates/developers/extensions/edit/details.hbs +16 -0
  78. package/addon/templates/developers/extensions/edit/index.hbs +1 -0
  79. package/addon/templates/developers/extensions/edit/monetize.hbs +3 -0
  80. package/addon/templates/developers/extensions/edit.hbs +48 -0
  81. package/addon/templates/developers/extensions/index.hbs +27 -0
  82. package/addon/templates/developers/extensions/new.hbs +39 -0
  83. package/addon/templates/developers/extensions.hbs +1 -0
  84. package/addon/templates/developers/payments/index.hbs +33 -0
  85. package/addon/templates/developers/payments/onboard.hbs +48 -0
  86. package/addon/templates/developers/payments.hbs +1 -0
  87. package/addon/templates/developers.hbs +1 -0
  88. package/addon/templates/explore/category.hbs +12 -0
  89. package/addon/templates/explore/index.hbs +12 -0
  90. package/addon/templates/explore.hbs +1 -0
  91. package/addon/templates/installed.hbs +32 -0
  92. package/addon/templates/purchased.hbs +34 -0
  93. package/app/adapters/registry-bridge.js +1 -0
  94. package/app/adapters/registry-extension-bundle.js +1 -0
  95. package/app/adapters/registry-extension.js +1 -0
  96. package/app/components/extension-card.js +1 -0
  97. package/app/components/extension-form.js +1 -0
  98. package/app/components/extension-modal-title.js +1 -0
  99. package/app/components/extension-monetize-form.js +1 -0
  100. package/app/components/extension-pending-publish-viewer.js +1 -0
  101. package/app/components/extension-reviewer-control.js +1 -0
  102. package/app/components/modals/confirm-extension-purchase.js +1 -0
  103. package/app/components/modals/extension-details.js +1 -0
  104. package/app/components/modals/extension-purchase-form.js +1 -0
  105. package/app/components/modals/extension-uninstall.js +1 -0
  106. package/app/components/modals/select-extension-bundle.js +1 -0
  107. package/app/components/progress-bar.js +1 -0
  108. package/app/controllers/application.js +1 -0
  109. package/app/controllers/developers/analytics.js +1 -0
  110. package/app/controllers/developers/extensions/edit/bundles.js +1 -0
  111. package/app/controllers/developers/extensions/edit/index.js +1 -0
  112. package/app/controllers/developers/extensions/edit/monetize.js +1 -0
  113. package/app/controllers/developers/extensions/edit.js +1 -0
  114. package/app/controllers/developers/extensions/index.js +1 -0
  115. package/app/controllers/developers/extensions/new.js +1 -0
  116. package/app/controllers/developers/payments/index.js +1 -0
  117. package/app/controllers/developers/payments/onboard.js +1 -0
  118. package/app/controllers/explore/category.js +1 -0
  119. package/app/controllers/explore/index.js +1 -0
  120. package/app/controllers/installed.js +1 -0
  121. package/app/controllers/purchased.js +1 -0
  122. package/app/models/registry-extension-bundle.js +1 -0
  123. package/app/models/registry-extension.js +1 -0
  124. package/app/routes/developers/analytics.js +1 -0
  125. package/app/routes/developers/credentials.js +1 -0
  126. package/app/routes/developers/extensions/edit/bundles.js +1 -0
  127. package/app/routes/developers/extensions/edit/details.js +1 -0
  128. package/app/routes/developers/extensions/edit/index.js +1 -0
  129. package/app/routes/developers/extensions/edit/monetize.js +1 -0
  130. package/app/routes/developers/extensions/edit.js +1 -0
  131. package/app/routes/developers/extensions/index.js +1 -0
  132. package/app/routes/developers/extensions/new.js +1 -0
  133. package/app/routes/developers/extensions.js +1 -0
  134. package/app/routes/developers/payments/index.js +1 -0
  135. package/app/routes/developers/payments/onboard.js +1 -0
  136. package/app/routes/developers/payments.js +1 -0
  137. package/app/routes/developers.js +1 -0
  138. package/app/routes/explore/category.js +1 -0
  139. package/app/routes/explore/index.js +1 -0
  140. package/app/routes/explore.js +1 -0
  141. package/app/routes/installed.js +1 -0
  142. package/app/routes/purchased.js +1 -0
  143. package/app/serializers/registry-extension-bundle.js +1 -0
  144. package/app/serializers/registry-extension.js +1 -0
  145. package/app/services/stripe.js +1 -0
  146. package/app/templates/developers/analytics.js +1 -0
  147. package/app/templates/developers/credentials.js +1 -0
  148. package/app/templates/developers/extensions/edit/bundles.js +1 -0
  149. package/app/templates/developers/extensions/edit/details.js +1 -0
  150. package/app/templates/developers/extensions/edit/index.js +1 -0
  151. package/app/templates/developers/extensions/edit/monetize.js +1 -0
  152. package/app/templates/developers/extensions/edit.js +1 -0
  153. package/app/templates/developers/extensions/index.js +1 -0
  154. package/app/templates/developers/extensions/new.js +1 -0
  155. package/app/templates/developers/extensions.js +1 -0
  156. package/app/templates/developers/payments/index.js +1 -0
  157. package/app/templates/developers/payments/onboard.js +1 -0
  158. package/app/templates/developers/payments.js +1 -0
  159. package/app/templates/developers.js +1 -0
  160. package/app/templates/explore/category.js +1 -0
  161. package/app/templates/explore/index.js +1 -0
  162. package/app/templates/explore.js +1 -0
  163. package/app/templates/installed.js +1 -0
  164. package/app/templates/purchased.js +1 -0
  165. package/composer.json +95 -0
  166. package/config/environment.js +28 -0
  167. package/extension.json +10 -0
  168. package/index.js +26 -0
  169. package/package.json +129 -0
  170. package/phpstan.neon.dist +8 -0
  171. package/phpunit.xml.dist +16 -0
  172. package/server/.gitattributes +14 -0
  173. package/server/config/registry-bridge.php +32 -0
  174. package/server/migrations/2024_03_19_060627_create_registry_users_table.php +42 -0
  175. package/server/migrations/2024_03_21_051614_create_registry_extensions_table.php +76 -0
  176. package/server/migrations/2024_03_25_044537_create_registry_extension_bundles_table.php +54 -0
  177. package/server/migrations/2024_03_29_072101_registry_extension_installs.php +35 -0
  178. package/server/migrations/2024_07_16_155000_create_registry_extension_purchases.php +41 -0
  179. package/server/seeders/ExtensionsCategorySeeder.php +359 -0
  180. package/server/src/Console/Commands/Initialize.php +35 -0
  181. package/server/src/Console/Commands/PostInstallExtension.php +84 -0
  182. package/server/src/Exceptions/InstallFailedException.php +21 -0
  183. package/server/src/Expansions/CategoryExpansion.php +30 -0
  184. package/server/src/Http/Controllers/Internal/v1/ExtensionInstallerController.php +153 -0
  185. package/server/src/Http/Controllers/Internal/v1/RegistryAuthController.php +230 -0
  186. package/server/src/Http/Controllers/Internal/v1/RegistryController.php +54 -0
  187. package/server/src/Http/Controllers/Internal/v1/RegistryExtensionBundleController.php +112 -0
  188. package/server/src/Http/Controllers/Internal/v1/RegistryExtensionController.php +257 -0
  189. package/server/src/Http/Controllers/Internal/v1/RegistryPaymentsController.php +227 -0
  190. package/server/src/Http/Controllers/RegistryBridgeController.php +13 -0
  191. package/server/src/Http/Filter/RegistryExtensionFilter.php +80 -0
  192. package/server/src/Http/Requests/AddRegistryUserRequest.php +47 -0
  193. package/server/src/Http/Requests/AuthenticateRegistryUserRequest.php +47 -0
  194. package/server/src/Http/Requests/CreateRegistryExtensionBundleRequest.php +42 -0
  195. package/server/src/Http/Requests/CreateRegistryExtensionRequest.php +31 -0
  196. package/server/src/Http/Requests/InstallExtensionRequest.php +30 -0
  197. package/server/src/Http/Requests/RegistryAuthRequest.php +46 -0
  198. package/server/src/Http/Requests/RegistryExtensionActionRequest.php +30 -0
  199. package/server/src/Http/Resources/RegistryUser.php +40 -0
  200. package/server/src/Models/RegistryExtension.php +656 -0
  201. package/server/src/Models/RegistryExtensionBundle.php +1015 -0
  202. package/server/src/Models/RegistryExtensionInstall.php +76 -0
  203. package/server/src/Models/RegistryExtensionPurchase.php +87 -0
  204. package/server/src/Models/RegistryUser.php +140 -0
  205. package/server/src/Providers/RegistryBridgeServiceProvider.php +117 -0
  206. package/server/src/Support/Bridge.php +53 -0
  207. package/server/src/Support/Utils.php +19 -0
  208. package/server/src/routes.php +58 -0
  209. package/server/tests/Feature.php +5 -0
  210. package/translations/en-us.yaml +119 -0
  211. package/tsconfig.declarations.json +10 -0
@@ -0,0 +1,656 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\RegistryBridge\Models;
4
+
5
+ use Fleetbase\Casts\Json;
6
+ use Fleetbase\Casts\Money;
7
+ use Fleetbase\Models\Category;
8
+ use Fleetbase\Models\Company;
9
+ use Fleetbase\Models\File;
10
+ use Fleetbase\Models\Model;
11
+ use Fleetbase\Models\User;
12
+ use Fleetbase\RegistryBridge\Support\Utils;
13
+ use Fleetbase\Support\Utils as SupportUtils;
14
+ use Fleetbase\Traits\HasApiModelBehavior;
15
+ use Fleetbase\Traits\HasMetaAttributes;
16
+ use Fleetbase\Traits\HasPublicId;
17
+ use Fleetbase\Traits\HasUuid;
18
+ use Fleetbase\Traits\Searchable;
19
+ use Spatie\Sluggable\HasSlug;
20
+ use Spatie\Sluggable\SlugOptions;
21
+
22
+ class RegistryExtension extends Model
23
+ {
24
+ use HasUuid;
25
+ use HasPublicId;
26
+ use HasMetaAttributes;
27
+ use HasApiModelBehavior;
28
+ use HasSlug;
29
+ use Searchable;
30
+
31
+ /**
32
+ * The database table used by the model.
33
+ *
34
+ * @var string
35
+ */
36
+ protected $table = 'registry_extensions';
37
+
38
+ /**
39
+ * The type of public Id to generate.
40
+ *
41
+ * @var string
42
+ */
43
+ protected $publicIdType = 'extension';
44
+
45
+ /**
46
+ * The attributes that are mass assignable.
47
+ */
48
+ protected $fillable = [
49
+ 'uuid',
50
+ 'company_uuid',
51
+ 'created_by_uuid',
52
+ 'category_uuid',
53
+ 'registry_user_uuid',
54
+ 'current_bundle_uuid',
55
+ 'next_bundle_uuid',
56
+ 'icon_uuid',
57
+ 'public_id',
58
+ 'stripe_product_id',
59
+ 'name',
60
+ 'subtitle',
61
+ 'payment_required',
62
+ 'price',
63
+ 'sale_price',
64
+ 'on_sale',
65
+ 'subscription_required',
66
+ 'subscription_billing_period',
67
+ 'subscription_model',
68
+ 'subscription_amount',
69
+ 'subscription_tiers',
70
+ 'currency',
71
+ 'slug',
72
+ 'version',
73
+ 'fa_icon',
74
+ 'description',
75
+ 'promotional_text',
76
+ 'website_url',
77
+ 'repo_url',
78
+ 'support_url',
79
+ 'privacy_policy_url',
80
+ 'tos_url',
81
+ 'copyright',
82
+ 'primary_language',
83
+ 'tags',
84
+ 'languages',
85
+ 'meta',
86
+ 'core_extension',
87
+ 'status',
88
+ ];
89
+
90
+ /**
91
+ * The attributes that should be cast to native types.
92
+ */
93
+ protected $casts = [
94
+ 'payment_required' => 'boolean',
95
+ 'on_sale' => 'boolean',
96
+ 'subscription_required' => 'boolean',
97
+ 'subscription_tiers' => Json::class,
98
+ 'tags' => Json::class,
99
+ 'languages' => Json::class,
100
+ 'meta' => Json::class,
101
+ 'core_extension' => 'boolean',
102
+ 'price' => Money::class,
103
+ 'sale_price' => Money::class,
104
+ 'subscription_amount' => Money::class,
105
+ ];
106
+
107
+ /**
108
+ * Dynamic attributes that are appended to object.
109
+ *
110
+ * @var array
111
+ */
112
+ protected $appends = [
113
+ 'icon_url',
114
+ 'current_bundle_filename',
115
+ 'current_bundle_id',
116
+ 'current_bundle_public_id',
117
+ 'next_bundle_filename',
118
+ 'next_bundle_id',
119
+ 'next_bundle_public_id',
120
+ 'category_name',
121
+ 'publisher_name',
122
+ 'is_purchased',
123
+ 'is_installed',
124
+ ];
125
+
126
+ /**
127
+ * Relations that should be loaded with model.
128
+ *
129
+ * @var array
130
+ */
131
+ protected $with = [
132
+ 'category',
133
+ ];
134
+
135
+ /**
136
+ * Relations that should not be loaded.
137
+ *
138
+ * @var array
139
+ */
140
+ protected $without = [
141
+ 'current_bundle',
142
+ 'next_bundle',
143
+ ];
144
+
145
+ /**
146
+ * Searchable columns.
147
+ *
148
+ * @var array
149
+ */
150
+ protected $searchableColumns = ['name'];
151
+
152
+ /**
153
+ * The "booting" method of the model.
154
+ *
155
+ * This method is called on the model boot and sets up
156
+ * event listeners, such as creating a unique bundle ID
157
+ * when a new model instance is being created.
158
+ */
159
+ protected static function boot()
160
+ {
161
+ parent::boot();
162
+
163
+ static::saving(function ($model) {
164
+ if ($model->isDirty('price')) {
165
+ $model->updateOrCreateStripePrice();
166
+ }
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Get the options for generating the slug.
172
+ */
173
+ public function getSlugOptions(): SlugOptions
174
+ {
175
+ return SlugOptions::create()
176
+ ->generateSlugsFrom('name')
177
+ ->saveSlugsTo('slug');
178
+ }
179
+
180
+ /**
181
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
182
+ */
183
+ public function company()
184
+ {
185
+ return $this->belongsTo(Company::class, 'company_uuid', 'uuid');
186
+ }
187
+
188
+ /**
189
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
190
+ */
191
+ public function createdBy()
192
+ {
193
+ return $this->belongsTo(User::class, 'created_by_uuid', 'uuid');
194
+ }
195
+
196
+ /**
197
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
198
+ */
199
+ public function registryUser()
200
+ {
201
+ return $this->belongsTo(RegistryUser::class, 'registry_user_uuid', 'uuid');
202
+ }
203
+
204
+ /**
205
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
206
+ */
207
+ public function currentBundle()
208
+ {
209
+ return $this->belongsTo(RegistryExtensionBundle::class, 'current_bundle_uuid', 'uuid');
210
+ }
211
+
212
+ /**
213
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
214
+ */
215
+ public function nextBundle()
216
+ {
217
+ return $this->belongsTo(RegistryExtensionBundle::class, 'next_bundle_uuid', 'uuid');
218
+ }
219
+
220
+ /**
221
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
222
+ */
223
+ public function icon()
224
+ {
225
+ return $this->belongsTo(File::class, 'icon_uuid', 'uuid');
226
+ }
227
+
228
+ /**
229
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
230
+ */
231
+ public function category()
232
+ {
233
+ return $this->belongsTo(Category::class, 'category_uuid', 'uuid');
234
+ }
235
+
236
+ /**
237
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
238
+ */
239
+ public function screenshots()
240
+ {
241
+ return $this->hasMany(File::class, 'subject_uuid', 'uuid')->where('type', 'extension_screenshot');
242
+ }
243
+
244
+ /**
245
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
246
+ */
247
+ public function bundles()
248
+ {
249
+ return $this->hasMany(RegistryExtensionBundle::class, 'extension_uuid', 'uuid');
250
+ }
251
+
252
+ /**
253
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
254
+ */
255
+ public function bundleFiles()
256
+ {
257
+ return $this->hasMany(File::class, 'subject_uuid', 'uuid')->where('type', 'extension_bundle');
258
+ }
259
+
260
+ /**
261
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
262
+ */
263
+ public function installs()
264
+ {
265
+ return $this->hasMany(RegistryExtensionInstall::class, 'extension_uuid', 'uuid');
266
+ }
267
+
268
+ /**
269
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
270
+ */
271
+ public function purchases()
272
+ {
273
+ return $this->hasMany(RegistryExtensionPurchase::class, 'extension_uuid', 'uuid');
274
+ }
275
+
276
+ /**
277
+ * Check if current session has purchased extension.
278
+ */
279
+ public function getIsPurchasedAttribute(): bool
280
+ {
281
+ return $this->purchases->contains(function ($purchase) {
282
+ return $purchase->company_uuid === session('company');
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Check if current session has installed extension.
288
+ */
289
+ public function getIsInstalledAttribute(): bool
290
+ {
291
+ return $this->installs->contains(function ($purchase) {
292
+ return $purchase->company_uuid === session('company');
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Get avatar URL attribute.
298
+ */
299
+ public function getIconUrlAttribute(): ?string
300
+ {
301
+ if ($this->icon instanceof File) {
302
+ return $this->icon->url;
303
+ }
304
+
305
+ return data_get($this, 'icon.url', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/default-extension-icon.svg');
306
+ }
307
+
308
+ /**
309
+ * Get the current bundle public ID.
310
+ */
311
+ public function getCurrentBundlePublicIdAttribute(): ?string
312
+ {
313
+ if ($this->currentBundle instanceof RegistryExtensionBundle) {
314
+ return $this->currentBundle->public_id;
315
+ }
316
+
317
+ return data_get($this, 'currentBundle.public_id');
318
+ }
319
+
320
+ /**
321
+ * Get the current bundle ID.
322
+ */
323
+ public function getCurrentBundleIdAttribute(): ?string
324
+ {
325
+ if ($this->currentBundle instanceof RegistryExtensionBundle) {
326
+ return $this->currentBundle->bundle_id;
327
+ }
328
+
329
+ return data_get($this, 'currentBundle.bundle_id');
330
+ }
331
+
332
+ /**
333
+ * Get the current bundle original filename.
334
+ */
335
+ public function getCurrentBundleFilenameAttribute(): ?string
336
+ {
337
+ if ($this->currentBundle instanceof RegistryExtensionBundle) {
338
+ return $this->currentBundle->bundle_filename;
339
+ }
340
+
341
+ return data_get($this, 'currentBundle.bundle_filename');
342
+ }
343
+
344
+ /**
345
+ * Get the next bundle public ID.
346
+ */
347
+ public function getNextBundlePublicIdAttribute(): ?string
348
+ {
349
+ if ($this->nextBundle instanceof RegistryExtensionBundle) {
350
+ return $this->nextBundle->public_id;
351
+ }
352
+
353
+ return data_get($this, 'nextBundle.public_id');
354
+ }
355
+
356
+ /**
357
+ * Get the next bundle ID.
358
+ */
359
+ public function getNextBundleIdAttribute(): ?string
360
+ {
361
+ if ($this->nextBundle instanceof RegistryExtensionBundle) {
362
+ return $this->nextBundle->bundle_id;
363
+ }
364
+
365
+ return data_get($this, 'nextBundle.bundle_id');
366
+ }
367
+
368
+ /**
369
+ * Get the current bundle original filename.
370
+ */
371
+ public function getNextBundleFilenameAttribute(): ?string
372
+ {
373
+ if ($this->nextBundle instanceof RegistryExtensionBundle) {
374
+ return $this->nextBundle->bundle_filename;
375
+ }
376
+
377
+ return data_get($this, 'nextBundle.bundle_filename');
378
+ }
379
+
380
+ /**
381
+ * Get the extension's category.
382
+ */
383
+ public function getCategoryNameAttribute(): ?string
384
+ {
385
+ if ($this->category instanceof Category) {
386
+ return $this->category->name;
387
+ }
388
+
389
+ return data_get($this, 'category.name');
390
+ }
391
+
392
+ /**
393
+ * Get the extension's category.
394
+ */
395
+ public function getPublisherNameAttribute(): ?string
396
+ {
397
+ if ($this->company instanceof Company) {
398
+ return $this->company->name;
399
+ }
400
+
401
+ return data_get($this, 'company.name');
402
+ }
403
+
404
+ /**
405
+ * Finds a RegistryExtension by package name in the associated currentBundle.
406
+ *
407
+ * This method searches for a RegistryExtension where the associated currentBundle's
408
+ * 'meta' JSON column contains the specified package name either in the 'api' field
409
+ * or the 'engine' field. It returns the first matching RegistryExtension or null
410
+ * if no matches are found. The method leverages Eloquent's relationship querying
411
+ * capabilities to efficiently filter the results.
412
+ *
413
+ * @param string $packageName the name of the package to search for in the 'api' or 'engine' fields
414
+ *
415
+ * @return RegistryExtension|null the first RegistryExtension that matches the search criteria, or null if no match is found
416
+ */
417
+ public static function findByPackageName(string $packageName): ?RegistryExtension
418
+ {
419
+ return static::whereHas('currentBundle', function ($query) use ($packageName) {
420
+ $query->where('meta->package.json->name', $packageName)->orWhere('meta->composer.json->name', $packageName);
421
+ })->first();
422
+ }
423
+
424
+ /**
425
+ * Determines if the current extension instance is ready for submission.
426
+ *
427
+ * This method is an instance method that internally calls the static method
428
+ * `isExtensionReadyForSubmission` to perform the validation on the current instance.
429
+ * It checks various fields of the extension for certain criteria such as minimum
430
+ * string lengths, presence of necessary fields, and URL validation.
431
+ *
432
+ * @return bool returns true if the extension instance passes all validations, false otherwise
433
+ */
434
+ public function isReadyForSubmission(): bool
435
+ {
436
+ return static::isExtensionReadyForSubmission($this);
437
+ }
438
+
439
+ /**
440
+ * Validates if an extension, identified by its ID or instance, is ready for submission.
441
+ *
442
+ * This method accepts either an extension ID or an instance of `RegistryExtension`. It then
443
+ * performs various validations on fields like 'name', 'description', 'tags', etc., to
444
+ * determine if the extension meets the criteria for submission. URL fields are validated
445
+ * to ensure they contain proper URLs. The method is designed to be flexible, handling
446
+ * validation for both an extension ID and an extension object.
447
+ *
448
+ * @param int|RegistryExtension $extensionId the ID of the extension or an instance of `RegistryExtension`
449
+ *
450
+ * @return bool returns true if the extension passes all validations, false otherwise
451
+ *
452
+ * @throws \Illuminate\Database\Eloquent\ModelNotFoundException if the extension with the given ID is not found
453
+ */
454
+ public static function isExtensionReadyForSubmission($extensionId): bool
455
+ {
456
+ if ($extensionId instanceof RegistryExtension) {
457
+ $extension = $extensionId;
458
+ } else {
459
+ $extension = self::find($extensionId);
460
+ }
461
+ if (!$extension) {
462
+ return false;
463
+ }
464
+
465
+ $validations = [
466
+ 'name' => function ($value) {
467
+ return is_string($value) && strlen($value) > 3;
468
+ },
469
+ 'description' => function ($value) {
470
+ return is_string($value) && strlen($value) > 12;
471
+ },
472
+ 'tags' => function ($value) {
473
+ return !empty($value);
474
+ },
475
+ 'promotional_text' => function ($value) {
476
+ return !empty($value);
477
+ },
478
+ 'subtitle' => function ($value) {
479
+ return !empty($value);
480
+ },
481
+ 'copyright' => function ($value) {
482
+ return !empty($value);
483
+ },
484
+ 'website_url' => function ($value) {
485
+ return filter_var($value, FILTER_VALIDATE_URL) !== false;
486
+ },
487
+ 'support_url' => function ($value) {
488
+ return filter_var($value, FILTER_VALIDATE_URL) !== false;
489
+ },
490
+ 'privacy_policy_url' => function ($value) {
491
+ return filter_var($value, FILTER_VALIDATE_URL) !== false;
492
+ },
493
+ 'icon_uuid' => function ($value) {
494
+ return !empty($value);
495
+ },
496
+ 'category_uuid' => function ($value) {
497
+ return !empty($value);
498
+ },
499
+ 'next_bundle_uuid' => function ($value) {
500
+ return !empty($value);
501
+ },
502
+ ];
503
+
504
+ // Should have a new bundle for submission
505
+ $isNewBundle = $extension->next_bundle_uuid !== $extension->current_bundle_uuid;
506
+ if (!$isNewBundle) {
507
+ return false;
508
+ }
509
+
510
+ // Check validations
511
+ foreach ($validations as $field => $validationFunction) {
512
+ if (isset($extension->$field)) {
513
+ $value = $extension->$field;
514
+ if (!$validationFunction($value)) {
515
+ return false;
516
+ }
517
+ } else {
518
+ return false;
519
+ }
520
+ }
521
+
522
+ return true;
523
+ }
524
+
525
+ /**
526
+ * Retrieves or creates a Stripe product associated with this model instance.
527
+ *
528
+ * This method attempts to fetch a Stripe product based on an existing 'stripe_product_id'.
529
+ * If no product is found or if 'stripe_product_id' is not set, it creates a new Stripe product
530
+ * with the current model's name and description and updates the model's 'stripe_product_id'.
531
+ *
532
+ * @return \Stripe\Product|null returns the Stripe Product object if successful, or null if the Stripe client is not available
533
+ */
534
+ public function getStripeProduct(): ?\Stripe\Product
535
+ {
536
+ $stripe = Utils::getStripeClient();
537
+ if ($stripe) {
538
+ $product = $this->stripe_product_id ? $stripe->products->retrieve($this->stripe_product_id, []) : null;
539
+ if (!$product) {
540
+ $product = $stripe->products->create(['name' => $this->name, 'description' => $this->description, 'metadata' => ['fleetbase_id' => $this->public_id]]);
541
+ }
542
+
543
+ // Update stripe product id
544
+ $this->update(['stripe_product_id' => $product->id]);
545
+
546
+ return $product;
547
+ }
548
+
549
+ return null;
550
+ }
551
+
552
+ /**
553
+ * Retrieves the active Stripe price for the associated product.
554
+ *
555
+ * This method first ensures the product exists in Stripe by calling getStripeProduct().
556
+ * It then fetches the current active price for the product. If no active prices are found,
557
+ * it returns null.
558
+ *
559
+ * @return \Stripe\Price|null returns the Stripe Price object if available, or null otherwise
560
+ */
561
+ public function getStripePrice(): ?\Stripe\Price
562
+ {
563
+ if (!$this->stripe_product_id) {
564
+ $this->getStripeProduct();
565
+ }
566
+
567
+ $stripe = Utils::getStripeClient();
568
+ $price = null;
569
+ if ($stripe) {
570
+ $prices = $stripe->prices->all(['product' => $this->stripe_product_id, 'limit' => 1, 'active' => true]);
571
+ $price = is_array($prices->data) && count($prices->data) ? $prices->data[0] : null;
572
+ }
573
+
574
+ return $price;
575
+ }
576
+
577
+ /**
578
+ * Updates the existing active Stripe price to inactive and creates a new Stripe price.
579
+ *
580
+ * This method first retrieves the current active price and deactivates it if it exists.
581
+ * It then creates a new price with the current model's price and currency, associated with
582
+ * the Stripe product.
583
+ *
584
+ * @return \Stripe\Price|null returns the newly created Stripe Price object
585
+ */
586
+ public function updateOrCreateStripePrice(): ?\Stripe\Price
587
+ {
588
+ $stripe = Utils::getStripeClient();
589
+ $price = $this->getStripePrice();
590
+ if ($price instanceof \Stripe\Price) {
591
+ // update stripe price
592
+ $stripe->prices->update($price->id, ['active' => false]);
593
+ }
594
+
595
+ // create new stripe price
596
+ $price = $stripe->prices->create(['unit_amount' => $this->price, 'currency' => $this->currency, 'product' => $this->stripe_product_id]);
597
+
598
+ return $price;
599
+ }
600
+
601
+ /**
602
+ * Creates a Stripe Checkout session for purchasing this model's associated product.
603
+ *
604
+ * This method ensures that the model has a valid company (extension author) and an active price.
605
+ * It calculates the facilitator fee based on the total amount and creates a checkout session with
606
+ * the necessary Stripe configurations, including the return URI with query parameters.
607
+ *
608
+ * @param string $returnUri the URI to which the user should be returned after the checkout process
609
+ *
610
+ * @return \Stripe\Checkout\Session returns the Stripe Checkout Session object
611
+ *
612
+ * @throws \Exception throws an exception if the model does not have an associated company or price
613
+ */
614
+ public function createStripeCheckoutSession(string $returnUri): \Stripe\Checkout\Session
615
+ {
616
+ // Get extension author
617
+ $extensionAuthor = $this->company;
618
+ if (!$extensionAuthor) {
619
+ throw new \Exception('The extension you attempted to purchase is not available for purchase at this time.');
620
+ }
621
+
622
+ // Get the extension price from stripe
623
+ $price = $this->getStripePrice();
624
+ if (!$price) {
625
+ throw new \Exception('The extension you attempted to purchase is not available for purchase at this time.');
626
+ }
627
+
628
+ // Calculate the fee fleetbase takes for faciliation of extension
629
+ $totalAmount = $price->unit_amount;
630
+ $facilitatorFee = SupportUtils::calculatePercentage(config('registry-bridge.facilitator_fee', 10), $totalAmount);
631
+
632
+ // Get the stripe client to create the checkout session
633
+ $stripe = Utils::getStripeClient();
634
+
635
+ // Create the stripe checkout session
636
+ $checkoutSession = $stripe->checkout->sessions->create([
637
+ 'ui_mode' => 'embedded',
638
+ 'line_items' => [
639
+ [
640
+ 'price' => $price->id,
641
+ 'quantity' => 1,
642
+ ],
643
+ ],
644
+ 'mode' => 'payment',
645
+ 'return_url' => SupportUtils::consoleUrl($returnUri) . '?extension_id=' . $this->uuid . '&checkout_session_id={CHECKOUT_SESSION_ID}',
646
+ 'payment_intent_data' => [
647
+ 'application_fee_amount' => $facilitatorFee,
648
+ 'transfer_data' => [
649
+ 'destination' => $extensionAuthor->stripe_connect_id,
650
+ ],
651
+ ],
652
+ ]);
653
+
654
+ return $checkoutSession;
655
+ }
656
+ }