@subvalue/cli 0.1.4 → 0.1.6
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.
- package/PRE_RELEASE.md +1 -1
- package/README.md +6 -2
- package/dist/packages/cli/src/server.js +75 -28
- package/dist/packages/core/src/billing.js +112 -0
- package/dist/packages/core/src/summary.js +69 -68
- package/dist/packages/core/src/types.js +11 -2
- package/dist/packages/receipt/src/index.js +165 -17
- package/dist/packages/ui/src/brand.js +1 -0
- package/dist/public/app.css +210 -2
- package/dist/public/apps/web/src/landing.js +20 -5
- package/dist/public/apps/web/src/main.js +268 -69
- package/dist/public/landing.css +265 -29
- package/dist/public/packages/core/src/billing.js +112 -0
- package/dist/public/packages/receipt/src/index.js +165 -17
- package/dist/public/packages/ui/src/brand.js +1 -0
- package/package.json +1 -1
package/dist/public/landing.css
CHANGED
|
@@ -295,6 +295,35 @@ p {
|
|
|
295
295
|
gap: 20px;
|
|
296
296
|
margin-bottom: 22px;
|
|
297
297
|
}
|
|
298
|
+
.overview-title {
|
|
299
|
+
display: flex;
|
|
300
|
+
flex-wrap: wrap;
|
|
301
|
+
align-items: baseline;
|
|
302
|
+
gap: 6px 14px;
|
|
303
|
+
min-width: 0;
|
|
304
|
+
}
|
|
305
|
+
.selected-period {
|
|
306
|
+
color: var(--green);
|
|
307
|
+
font-size: 17px;
|
|
308
|
+
font-weight: 550;
|
|
309
|
+
letter-spacing: -0.3px;
|
|
310
|
+
}
|
|
311
|
+
.empty-history {
|
|
312
|
+
color: var(--muted);
|
|
313
|
+
font-size: 12px;
|
|
314
|
+
line-height: 1.6;
|
|
315
|
+
margin: 10px 0 16px;
|
|
316
|
+
}
|
|
317
|
+
@media (max-width: 760px) {
|
|
318
|
+
.overview-title {
|
|
319
|
+
flex-direction: column;
|
|
320
|
+
align-items: start;
|
|
321
|
+
gap: 5px;
|
|
322
|
+
}
|
|
323
|
+
.selected-period {
|
|
324
|
+
font-size: 15px;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
298
327
|
.eyebrow {
|
|
299
328
|
font: 9px var(--mono);
|
|
300
329
|
letter-spacing: 1.7px;
|
|
@@ -2223,7 +2252,7 @@ dialog::backdrop {
|
|
|
2223
2252
|
}
|
|
2224
2253
|
}
|
|
2225
2254
|
.billing-row {
|
|
2226
|
-
grid-template-columns: 100px
|
|
2255
|
+
grid-template-columns: 100px minmax(0, 1fr);
|
|
2227
2256
|
gap: 16px;
|
|
2228
2257
|
align-items: start;
|
|
2229
2258
|
}
|
|
@@ -2242,7 +2271,7 @@ dialog::backdrop {
|
|
|
2242
2271
|
.amount-field input[readonly] {
|
|
2243
2272
|
color: var(--muted);
|
|
2244
2273
|
}
|
|
2245
|
-
.
|
|
2274
|
+
.billing-amount.hidden {
|
|
2246
2275
|
display: none;
|
|
2247
2276
|
}
|
|
2248
2277
|
.compact-details a {
|
|
@@ -2290,6 +2319,185 @@ dialog {
|
|
|
2290
2319
|
max-height: calc(100dvh - 32px);
|
|
2291
2320
|
overflow-y: auto;
|
|
2292
2321
|
}
|
|
2322
|
+
#expenses-dialog {
|
|
2323
|
+
width: min(440px, calc(100vw - 32px));
|
|
2324
|
+
max-height: calc(100dvh - 32px);
|
|
2325
|
+
overflow: auto;
|
|
2326
|
+
}
|
|
2327
|
+
.expenses-heading {
|
|
2328
|
+
display: flex;
|
|
2329
|
+
align-items: center;
|
|
2330
|
+
justify-content: space-between;
|
|
2331
|
+
gap: 12px;
|
|
2332
|
+
}
|
|
2333
|
+
.expenses-heading h2 {
|
|
2334
|
+
margin: 0;
|
|
2335
|
+
font-size: 18px;
|
|
2336
|
+
}
|
|
2337
|
+
.expenses-heading button {
|
|
2338
|
+
font-size: 24px;
|
|
2339
|
+
}
|
|
2340
|
+
.expense-month {
|
|
2341
|
+
display: flex;
|
|
2342
|
+
align-items: center;
|
|
2343
|
+
justify-content: space-between;
|
|
2344
|
+
gap: 16px;
|
|
2345
|
+
margin: 24px 0;
|
|
2346
|
+
color: var(--muted);
|
|
2347
|
+
font-size: 13px;
|
|
2348
|
+
}
|
|
2349
|
+
#expenses-dialog .billing-row {
|
|
2350
|
+
display: block;
|
|
2351
|
+
}
|
|
2352
|
+
#expenses-dialog .provider-setting-name {
|
|
2353
|
+
display: none;
|
|
2354
|
+
}
|
|
2355
|
+
#expenses-dialog .subscription-fields {
|
|
2356
|
+
display: flex;
|
|
2357
|
+
flex-direction: column;
|
|
2358
|
+
align-items: stretch;
|
|
2359
|
+
gap: 14px;
|
|
2360
|
+
}
|
|
2361
|
+
#expenses-dialog select {
|
|
2362
|
+
width: 100%;
|
|
2363
|
+
}
|
|
2364
|
+
.mixed-field {
|
|
2365
|
+
display: flex;
|
|
2366
|
+
align-items: center;
|
|
2367
|
+
gap: 8px;
|
|
2368
|
+
color: var(--muted);
|
|
2369
|
+
font-size: 12px;
|
|
2370
|
+
}
|
|
2371
|
+
.api-spend-field {
|
|
2372
|
+
display: flex;
|
|
2373
|
+
align-items: center;
|
|
2374
|
+
justify-content: space-between;
|
|
2375
|
+
gap: 16px;
|
|
2376
|
+
font-size: 13px;
|
|
2377
|
+
}
|
|
2378
|
+
.expense-note {
|
|
2379
|
+
color: var(--muted);
|
|
2380
|
+
font-size: 12px;
|
|
2381
|
+
margin: 20px 0;
|
|
2382
|
+
}
|
|
2383
|
+
.expense-actions {
|
|
2384
|
+
display: flex;
|
|
2385
|
+
align-items: center;
|
|
2386
|
+
justify-content: space-between;
|
|
2387
|
+
gap: 12px;
|
|
2388
|
+
}
|
|
2389
|
+
.expense-actions .button {
|
|
2390
|
+
width: auto;
|
|
2391
|
+
}
|
|
2392
|
+
#expense-error {
|
|
2393
|
+
color: var(--red);
|
|
2394
|
+
font-size: 12px;
|
|
2395
|
+
}
|
|
2396
|
+
.subscription-fields {
|
|
2397
|
+
flex-wrap: wrap;
|
|
2398
|
+
}
|
|
2399
|
+
.subscription-fields .mixed-field {
|
|
2400
|
+
flex-basis: 100%;
|
|
2401
|
+
}
|
|
2402
|
+
.hidden {
|
|
2403
|
+
display: none !important;
|
|
2404
|
+
}
|
|
2405
|
+
.provider-expenses-form {
|
|
2406
|
+
border-top: 1px solid var(--border);
|
|
2407
|
+
margin-top: 18px;
|
|
2408
|
+
padding-top: 16px;
|
|
2409
|
+
}
|
|
2410
|
+
.provider-expense-toggle {
|
|
2411
|
+
display: flex;
|
|
2412
|
+
align-items: center;
|
|
2413
|
+
justify-content: space-between;
|
|
2414
|
+
gap: 12px;
|
|
2415
|
+
width: 100%;
|
|
2416
|
+
min-height: 40px;
|
|
2417
|
+
margin: 14px 0 0;
|
|
2418
|
+
padding: 10px 12px;
|
|
2419
|
+
color: var(--green);
|
|
2420
|
+
background: #37ec850c;
|
|
2421
|
+
border: 1px solid #37ec8540;
|
|
2422
|
+
border-radius: 4px;
|
|
2423
|
+
font-size: 12px;
|
|
2424
|
+
text-align: left;
|
|
2425
|
+
line-height: 1.4;
|
|
2426
|
+
}
|
|
2427
|
+
.provider-expense-toggle:hover {
|
|
2428
|
+
background: #37ec8518;
|
|
2429
|
+
border-color: #37ec8580;
|
|
2430
|
+
}
|
|
2431
|
+
.provider-expense-toggle svg {
|
|
2432
|
+
width: 13px;
|
|
2433
|
+
height: 13px;
|
|
2434
|
+
flex-shrink: 0;
|
|
2435
|
+
}
|
|
2436
|
+
.provider-expense-toggle[aria-expanded='true'] {
|
|
2437
|
+
border-color: var(--green);
|
|
2438
|
+
}
|
|
2439
|
+
.provider-expenses-popover {
|
|
2440
|
+
position: fixed;
|
|
2441
|
+
inset: auto;
|
|
2442
|
+
margin: 0;
|
|
2443
|
+
width: min(350px, calc(100vw - 32px));
|
|
2444
|
+
max-height: calc(100dvh - 32px);
|
|
2445
|
+
overflow-y: auto;
|
|
2446
|
+
padding: 20px;
|
|
2447
|
+
color: var(--text);
|
|
2448
|
+
background: #14191e;
|
|
2449
|
+
border: 1px solid #3a4249;
|
|
2450
|
+
border-radius: 6px;
|
|
2451
|
+
box-shadow: 0 18px 48px #0009;
|
|
2452
|
+
}
|
|
2453
|
+
.provider-expenses-popover .expenses-heading h2 {
|
|
2454
|
+
font-size: 14px;
|
|
2455
|
+
}
|
|
2456
|
+
.provider-expenses-form .billing-row {
|
|
2457
|
+
display: block;
|
|
2458
|
+
}
|
|
2459
|
+
.provider-expenses-form .provider-setting-name {
|
|
2460
|
+
display: none;
|
|
2461
|
+
}
|
|
2462
|
+
.provider-expenses-form .subscription-fields {
|
|
2463
|
+
display: flex;
|
|
2464
|
+
flex-direction: column;
|
|
2465
|
+
align-items: stretch;
|
|
2466
|
+
gap: 12px;
|
|
2467
|
+
}
|
|
2468
|
+
.provider-expenses-form select {
|
|
2469
|
+
width: 100%;
|
|
2470
|
+
min-width: 0;
|
|
2471
|
+
}
|
|
2472
|
+
.provider-expenses-form .mixed-field {
|
|
2473
|
+
flex-basis: auto;
|
|
2474
|
+
}
|
|
2475
|
+
.provider-expenses-form .amount-field {
|
|
2476
|
+
width: fit-content;
|
|
2477
|
+
max-width: 100%;
|
|
2478
|
+
}
|
|
2479
|
+
.provider-expenses-form .api-spend-field .amount-field {
|
|
2480
|
+
width: 130px;
|
|
2481
|
+
}
|
|
2482
|
+
.provider-expenses-form .expense-actions {
|
|
2483
|
+
margin-top: 14px;
|
|
2484
|
+
}
|
|
2485
|
+
.provider-expenses-form small,
|
|
2486
|
+
.expense-period {
|
|
2487
|
+
color: var(--muted);
|
|
2488
|
+
font-size: 12px;
|
|
2489
|
+
}
|
|
2490
|
+
.billing-error {
|
|
2491
|
+
color: var(--red);
|
|
2492
|
+
font-size: 12px;
|
|
2493
|
+
}
|
|
2494
|
+
.settings-expenses {
|
|
2495
|
+
margin: 12px 0 22px;
|
|
2496
|
+
font-size: 12px;
|
|
2497
|
+
}
|
|
2498
|
+
.provider-grid {
|
|
2499
|
+
align-items: stretch;
|
|
2500
|
+
}
|
|
2293
2501
|
|
|
2294
2502
|
.hero h1 .brand-name {
|
|
2295
2503
|
font-weight: 850;
|
|
@@ -2314,13 +2522,27 @@ dialog {
|
|
|
2314
2522
|
justify-content: space-between;
|
|
2315
2523
|
align-items: center;
|
|
2316
2524
|
}
|
|
2317
|
-
.
|
|
2318
|
-
|
|
2525
|
+
.landing-links {
|
|
2526
|
+
display: flex;
|
|
2527
|
+
align-items: center;
|
|
2528
|
+
gap: 22px;
|
|
2529
|
+
}
|
|
2530
|
+
.github-link,
|
|
2531
|
+
.install-link {
|
|
2532
|
+
font-size: 13px;
|
|
2319
2533
|
color: var(--muted);
|
|
2320
2534
|
display: flex;
|
|
2535
|
+
align-items: center;
|
|
2536
|
+
min-height: 44px;
|
|
2321
2537
|
gap: 8px;
|
|
2322
2538
|
}
|
|
2323
|
-
.github-link span
|
|
2539
|
+
.github-link span,
|
|
2540
|
+
.install-link span {
|
|
2541
|
+
color: var(--green);
|
|
2542
|
+
}
|
|
2543
|
+
.github-link:hover,
|
|
2544
|
+
.install-link:hover,
|
|
2545
|
+
.trust-row a:hover {
|
|
2324
2546
|
color: var(--green);
|
|
2325
2547
|
}
|
|
2326
2548
|
.hero {
|
|
@@ -2393,13 +2615,16 @@ dialog {
|
|
|
2393
2615
|
}
|
|
2394
2616
|
.trust-row {
|
|
2395
2617
|
display: flex;
|
|
2618
|
+
flex-wrap: wrap;
|
|
2396
2619
|
justify-content: center;
|
|
2397
2620
|
gap: 25px;
|
|
2621
|
+
row-gap: 12px;
|
|
2398
2622
|
margin-top: 22px;
|
|
2399
2623
|
color: #a3b0a6;
|
|
2400
2624
|
font-size: 10px;
|
|
2401
2625
|
}
|
|
2402
|
-
.trust-row span
|
|
2626
|
+
.trust-row span,
|
|
2627
|
+
.trust-row a {
|
|
2403
2628
|
white-space: nowrap;
|
|
2404
2629
|
}
|
|
2405
2630
|
.release-note {
|
|
@@ -2415,7 +2640,7 @@ dialog {
|
|
|
2415
2640
|
.preview-label {
|
|
2416
2641
|
display: flex;
|
|
2417
2642
|
justify-content: space-between;
|
|
2418
|
-
font:
|
|
2643
|
+
font: 11px var(--mono);
|
|
2419
2644
|
letter-spacing: 1.2px;
|
|
2420
2645
|
color: var(--muted);
|
|
2421
2646
|
margin-bottom: 12px;
|
|
@@ -2433,7 +2658,7 @@ dialog {
|
|
|
2433
2658
|
gap: 29px;
|
|
2434
2659
|
padding: 20px 24px;
|
|
2435
2660
|
border-bottom: 1px solid var(--border);
|
|
2436
|
-
font-size:
|
|
2661
|
+
font-size: 12px;
|
|
2437
2662
|
color: var(--muted);
|
|
2438
2663
|
}
|
|
2439
2664
|
.preview-topbar .brand {
|
|
@@ -2458,7 +2683,7 @@ dialog {
|
|
|
2458
2683
|
}
|
|
2459
2684
|
.preview-date {
|
|
2460
2685
|
margin-left: auto;
|
|
2461
|
-
font:
|
|
2686
|
+
font: 11px var(--mono);
|
|
2462
2687
|
}
|
|
2463
2688
|
.preview-body {
|
|
2464
2689
|
display: grid;
|
|
@@ -2490,7 +2715,7 @@ dialog {
|
|
|
2490
2715
|
padding-top: 15px;
|
|
2491
2716
|
}
|
|
2492
2717
|
.preview-metrics span {
|
|
2493
|
-
font-size:
|
|
2718
|
+
font-size: 12px;
|
|
2494
2719
|
color: var(--muted);
|
|
2495
2720
|
}
|
|
2496
2721
|
.preview-metrics strong {
|
|
@@ -2525,7 +2750,7 @@ dialog {
|
|
|
2525
2750
|
font-weight: 500;
|
|
2526
2751
|
}
|
|
2527
2752
|
.preview-provider > div span {
|
|
2528
|
-
font-size:
|
|
2753
|
+
font-size: 12px;
|
|
2529
2754
|
color: var(--muted);
|
|
2530
2755
|
}
|
|
2531
2756
|
.preview-provider b {
|
|
@@ -2535,7 +2760,7 @@ dialog {
|
|
|
2535
2760
|
letter-spacing: -0.5px;
|
|
2536
2761
|
}
|
|
2537
2762
|
.preview-active {
|
|
2538
|
-
font:
|
|
2763
|
+
font: 11px var(--mono);
|
|
2539
2764
|
background: #153020;
|
|
2540
2765
|
border: 1px solid #284f35;
|
|
2541
2766
|
color: var(--green);
|
|
@@ -2552,16 +2777,16 @@ dialog {
|
|
|
2552
2777
|
display: flex;
|
|
2553
2778
|
justify-content: space-between;
|
|
2554
2779
|
gap: 12px;
|
|
2555
|
-
font-size:
|
|
2780
|
+
font-size: 12px;
|
|
2556
2781
|
margin-bottom: 19px;
|
|
2557
2782
|
}
|
|
2558
2783
|
.preview-chart strong {
|
|
2559
|
-
font-size:
|
|
2784
|
+
font-size: 13px;
|
|
2560
2785
|
font-weight: 450;
|
|
2561
2786
|
}
|
|
2562
2787
|
.preview-chart > div:first-child span {
|
|
2563
2788
|
color: var(--green);
|
|
2564
|
-
font-size:
|
|
2789
|
+
font-size: 11px;
|
|
2565
2790
|
}
|
|
2566
2791
|
.preview-bars {
|
|
2567
2792
|
height: 143px;
|
|
@@ -2587,7 +2812,7 @@ dialog {
|
|
|
2587
2812
|
.preview-chart-axis {
|
|
2588
2813
|
display: flex;
|
|
2589
2814
|
justify-content: space-between;
|
|
2590
|
-
font:
|
|
2815
|
+
font: 11px var(--mono);
|
|
2591
2816
|
color: var(--muted);
|
|
2592
2817
|
margin-top: 12px;
|
|
2593
2818
|
}
|
|
@@ -2604,6 +2829,9 @@ dialog {
|
|
|
2604
2829
|
display: flex;
|
|
2605
2830
|
flex-direction: column;
|
|
2606
2831
|
justify-content: start;
|
|
2832
|
+
min-width: 0;
|
|
2833
|
+
width: 100%;
|
|
2834
|
+
max-width: 368px;
|
|
2607
2835
|
gap: 13px;
|
|
2608
2836
|
}
|
|
2609
2837
|
.preview-receipt canvas {
|
|
@@ -2721,7 +2949,7 @@ dialog {
|
|
|
2721
2949
|
padding: 14px 10px;
|
|
2722
2950
|
}
|
|
2723
2951
|
.preview-metrics span {
|
|
2724
|
-
font-size:
|
|
2952
|
+
font-size: 12px;
|
|
2725
2953
|
}
|
|
2726
2954
|
}
|
|
2727
2955
|
@media (max-width: 760px) {
|
|
@@ -2767,13 +2995,13 @@ dialog {
|
|
|
2767
2995
|
margin-top: 13px;
|
|
2768
2996
|
}
|
|
2769
2997
|
.preview-label {
|
|
2770
|
-
font-size:
|
|
2998
|
+
font-size: 11px;
|
|
2771
2999
|
letter-spacing: 0.8px;
|
|
2772
3000
|
}
|
|
2773
3001
|
.preview-topbar {
|
|
2774
3002
|
padding: 18px;
|
|
2775
3003
|
gap: 19px;
|
|
2776
|
-
font-size:
|
|
3004
|
+
font-size: 12px;
|
|
2777
3005
|
}
|
|
2778
3006
|
.preview-topbar .brand {
|
|
2779
3007
|
font-size: 14px;
|
|
@@ -2783,7 +3011,7 @@ dialog {
|
|
|
2783
3011
|
display: none;
|
|
2784
3012
|
}
|
|
2785
3013
|
.preview-date {
|
|
2786
|
-
font-size:
|
|
3014
|
+
font-size: 11px;
|
|
2787
3015
|
}
|
|
2788
3016
|
.preview-body {
|
|
2789
3017
|
grid-template-columns: 1fr;
|
|
@@ -2802,7 +3030,7 @@ dialog {
|
|
|
2802
3030
|
font-size: 25px;
|
|
2803
3031
|
}
|
|
2804
3032
|
.preview-metrics span {
|
|
2805
|
-
font-size:
|
|
3033
|
+
font-size: 12px;
|
|
2806
3034
|
}
|
|
2807
3035
|
.preview-provider {
|
|
2808
3036
|
padding: 17px;
|
|
@@ -2897,7 +3125,7 @@ dialog {
|
|
|
2897
3125
|
color: #a8b7af;
|
|
2898
3126
|
}
|
|
2899
3127
|
.product-preview {
|
|
2900
|
-
max-width:
|
|
3128
|
+
max-width: 1240px;
|
|
2901
3129
|
}
|
|
2902
3130
|
.preview-window {
|
|
2903
3131
|
background: #0b0d10;
|
|
@@ -2933,7 +3161,7 @@ dialog {
|
|
|
2933
3161
|
letter-spacing: -0.9px;
|
|
2934
3162
|
}
|
|
2935
3163
|
.preview-metrics span {
|
|
2936
|
-
font-size:
|
|
3164
|
+
font-size: 12px;
|
|
2937
3165
|
color: #b7c0c7;
|
|
2938
3166
|
}
|
|
2939
3167
|
.preview-provider,
|
|
@@ -3039,13 +3267,13 @@ dialog {
|
|
|
3039
3267
|
padding-top: 16px;
|
|
3040
3268
|
}
|
|
3041
3269
|
.preview-metrics span {
|
|
3042
|
-
font-size:
|
|
3270
|
+
font-size: 12px;
|
|
3043
3271
|
}
|
|
3044
3272
|
.preview-metrics strong {
|
|
3045
3273
|
font-size: 27px;
|
|
3046
3274
|
}
|
|
3047
3275
|
.preview-receipt {
|
|
3048
|
-
max-width:
|
|
3276
|
+
max-width: 368px;
|
|
3049
3277
|
}
|
|
3050
3278
|
.preview-bars {
|
|
3051
3279
|
height: 138px;
|
|
@@ -3067,7 +3295,7 @@ dialog {
|
|
|
3067
3295
|
padding-bottom: 36px;
|
|
3068
3296
|
}
|
|
3069
3297
|
.preview-label {
|
|
3070
|
-
font-size:
|
|
3298
|
+
font-size: 11px;
|
|
3071
3299
|
}
|
|
3072
3300
|
.copy-command {
|
|
3073
3301
|
min-height: 40px;
|
|
@@ -3092,7 +3320,7 @@ dialog {
|
|
|
3092
3320
|
object-fit: contain;
|
|
3093
3321
|
}
|
|
3094
3322
|
.preview-body {
|
|
3095
|
-
grid-template-columns: minmax(0, 1fr)
|
|
3323
|
+
grid-template-columns: minmax(0, 1fr) 368px;
|
|
3096
3324
|
}
|
|
3097
3325
|
.preview-receipt canvas {
|
|
3098
3326
|
filter: drop-shadow(0 2px 2px #0007) drop-shadow(0 10px 14px #0006);
|
|
@@ -3107,9 +3335,17 @@ dialog {
|
|
|
3107
3335
|
.preview-bars i {
|
|
3108
3336
|
background: linear-gradient(to bottom, #37ec85, #278451);
|
|
3109
3337
|
}
|
|
3338
|
+
@media (max-width: 1100px) {
|
|
3339
|
+
.preview-metrics {
|
|
3340
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3110
3343
|
@media (max-width: 1000px) {
|
|
3111
3344
|
.preview-body {
|
|
3112
|
-
grid-template-columns:
|
|
3345
|
+
grid-template-columns: 1fr;
|
|
3346
|
+
}
|
|
3347
|
+
.preview-receipt {
|
|
3348
|
+
margin-inline: auto;
|
|
3113
3349
|
}
|
|
3114
3350
|
}
|
|
3115
3351
|
@media (max-width: 760px) {
|
|
@@ -3117,7 +3353,7 @@ dialog {
|
|
|
3117
3353
|
grid-template-columns: 1fr;
|
|
3118
3354
|
}
|
|
3119
3355
|
.preview-receipt {
|
|
3120
|
-
max-width:
|
|
3356
|
+
max-width: 368px;
|
|
3121
3357
|
}
|
|
3122
3358
|
.preview-topbar .brand-mark {
|
|
3123
3359
|
width: 20px;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { fullCalendarMonths, shiftCalendarMonth } from './calendar.js';
|
|
4
|
+
|
|
5
|
+
function civilDate(date ) {
|
|
6
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
7
|
+
}
|
|
8
|
+
export function billingPeriodKey(range ) {
|
|
9
|
+
return (
|
|
10
|
+
(range.calendarFrom ?? civilDate(new Date(range.from))) +
|
|
11
|
+
':' +
|
|
12
|
+
(range.calendarTo ?? civilDate(new Date(Date.parse(range.until) - 1)))
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
export function withPeriodBilling(
|
|
16
|
+
settings ,
|
|
17
|
+
provider ,
|
|
18
|
+
range ,
|
|
19
|
+
billing ,
|
|
20
|
+
) {
|
|
21
|
+
const next = structuredClone(settings),
|
|
22
|
+
exactKey = billingPeriodKey(range),
|
|
23
|
+
monthly = fullCalendarMonths(range) === 1,
|
|
24
|
+
key = monthly ? exactKey.slice(0, 7) : exactKey,
|
|
25
|
+
declarations = monthly ? (next.monthlyBilling ??= {}) : (next.periodBilling ??= {});
|
|
26
|
+
// A rolling range can share dates with a calendar month. Editing that month
|
|
27
|
+
// supersedes an earlier exact-date declaration instead of leaving a stale override.
|
|
28
|
+
if (monthly && next.periodBilling?.[exactKey]?.[provider]) {
|
|
29
|
+
delete next.periodBilling[exactKey][provider];
|
|
30
|
+
if (!Object.keys(next.periodBilling[exactKey]).length) delete next.periodBilling[exactKey];
|
|
31
|
+
}
|
|
32
|
+
declarations[key] ??= {};
|
|
33
|
+
if (billing) declarations[key][provider] = billing;
|
|
34
|
+
else delete declarations[key][provider];
|
|
35
|
+
if (!Object.keys(declarations[key]).length) delete declarations[key];
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
export function billingForSelection(settings , provider , range ) {
|
|
39
|
+
const exact = settings.periodBilling?.[billingPeriodKey(range)]?.[provider];
|
|
40
|
+
if (exact) return exact;
|
|
41
|
+
if (fullCalendarMonths(range) === 1)
|
|
42
|
+
return billingForMonth(settings, provider, billingPeriodKey(range).slice(0, 7));
|
|
43
|
+
return billingForMonth({ ...settings, monthlyBilling: undefined }, provider, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Keep the dashboard action limited to mixed billing or a second provider without a plan. */
|
|
47
|
+
export function showExpenseAction(
|
|
48
|
+
provider ,
|
|
49
|
+
providers ,
|
|
50
|
+
) {
|
|
51
|
+
if (!provider.records) return false;
|
|
52
|
+
if (provider.mode === 'MIXED') return true;
|
|
53
|
+
return (
|
|
54
|
+
(provider.mode === 'API' ||
|
|
55
|
+
provider.mode === 'NO_SUBSCRIPTION' ||
|
|
56
|
+
provider.mode === 'UNKNOWN') &&
|
|
57
|
+
providers.some(
|
|
58
|
+
(other) =>
|
|
59
|
+
other.provider !== provider.provider &&
|
|
60
|
+
other.records > 0 &&
|
|
61
|
+
(other.mode === 'SUBSCRIPTION' || other.mode === 'MIXED') &&
|
|
62
|
+
(other.monthly != null || (other.comparison.subscription ?? 0) > 0),
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A default is a user preference, not evidence of how a historical request was billed. */
|
|
68
|
+
export function billingForMonth(settings , provider , month ) {
|
|
69
|
+
const saved = settings.monthlyBilling?.[month]?.[provider] ?? settings.billing[provider];
|
|
70
|
+
return {
|
|
71
|
+
...saved,
|
|
72
|
+
mode: saved.mode === 'UNKNOWN' || saved.mode === 'NO_SUBSCRIPTION' ? 'API' : saved.mode,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function billingForRange(settings , provider , range ) {
|
|
77
|
+
const count = fullCalendarMonths(range);
|
|
78
|
+
const exact = settings.periodBilling?.[billingPeriodKey(range)]?.[provider];
|
|
79
|
+
const start = new Date(range.from);
|
|
80
|
+
const first =
|
|
81
|
+
range.calendarFrom?.slice(0, 7) ??
|
|
82
|
+
`${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}`;
|
|
83
|
+
const months = Array.from(
|
|
84
|
+
{ length: count ?? 1 },
|
|
85
|
+
(_, index) => exact ?? billingForMonth(settings, provider, shiftCalendarMonth(first, index) ),
|
|
86
|
+
);
|
|
87
|
+
const mode = months.every((b) => b.mode === months[0].mode)
|
|
88
|
+
? months[0].mode
|
|
89
|
+
: 'MIXED';
|
|
90
|
+
const subscriptionKnown = months.every((b) => b.mode === 'API' || b.monthly !== null);
|
|
91
|
+
const apiKnown = months.every((b) => b.mode === 'SUBSCRIPTION' || b.apiSpend != null);
|
|
92
|
+
const subscription =
|
|
93
|
+
count !== null && subscriptionKnown
|
|
94
|
+
? months.reduce((total, b) => total + (b.mode === 'API' ? 0 : b.monthly ), 0)
|
|
95
|
+
: null;
|
|
96
|
+
const apiSpend =
|
|
97
|
+
count !== null && apiKnown
|
|
98
|
+
? exact
|
|
99
|
+
? exact.mode === 'SUBSCRIPTION'
|
|
100
|
+
? 0
|
|
101
|
+
: exact.apiSpend
|
|
102
|
+
: months.reduce((total, b) => total + (b.mode === 'SUBSCRIPTION' ? 0 : b.apiSpend ), 0)
|
|
103
|
+
: null;
|
|
104
|
+
return {
|
|
105
|
+
mode,
|
|
106
|
+
monthly: months.length === 1 ? months[0].monthly : null,
|
|
107
|
+
subscription,
|
|
108
|
+
apiSpend,
|
|
109
|
+
paid: subscription !== null && apiSpend !== null ? subscription + apiSpend : null,
|
|
110
|
+
needsBilling: !subscriptionKnown || !apiKnown,
|
|
111
|
+
};
|
|
112
|
+
}
|