@devdash/bofrid-api-types 0.1.5

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 (163) hide show
  1. package/README.md +24 -0
  2. package/dist/app.d.ts +23 -0
  3. package/dist/dev.d.ts +6 -0
  4. package/dist/export-openapi.d.ts +9 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/lib/auth.d.ts +20 -0
  7. package/dist/lib/criipto-bankid.d.ts +45 -0
  8. package/dist/lib/datalake.d.ts +7 -0
  9. package/dist/lib/docs-filter.d.ts +15 -0
  10. package/dist/lib/email-action-token.d.ts +26 -0
  11. package/dist/lib/email-utm.d.ts +42 -0
  12. package/dist/lib/email.d.ts +94 -0
  13. package/dist/lib/env.d.ts +18 -0
  14. package/dist/lib/errors.d.ts +6 -0
  15. package/dist/lib/helpers.d.ts +6 -0
  16. package/dist/lib/logger.d.ts +25 -0
  17. package/dist/lib/markets.d.ts +1 -0
  18. package/dist/lib/org-number.d.ts +5 -0
  19. package/dist/lib/ownership.d.ts +47 -0
  20. package/dist/lib/pdf-watermark.d.ts +25 -0
  21. package/dist/lib/personnummer.d.ts +14 -0
  22. package/dist/lib/posthog.d.ts +51 -0
  23. package/dist/lib/premium.d.ts +13 -0
  24. package/dist/lib/profile-resolver.d.ts +14 -0
  25. package/dist/lib/redirect-state.d.ts +40 -0
  26. package/dist/lib/revalidate.d.ts +23 -0
  27. package/dist/lib/schemas.d.ts +21 -0
  28. package/dist/lib/sentry.d.ts +31 -0
  29. package/dist/lib/slug.d.ts +5 -0
  30. package/dist/lib/sms.d.ts +22 -0
  31. package/dist/lib/system-log.d.ts +31 -0
  32. package/dist/lib/webhook-events.d.ts +26 -0
  33. package/dist/lib/webhooks.d.ts +28 -0
  34. package/dist/middleware/auth-debug.d.ts +17 -0
  35. package/dist/middleware/auth.d.ts +19 -0
  36. package/dist/middleware/bibi-logger.d.ts +6 -0
  37. package/dist/middleware/cors.d.ts +1 -0
  38. package/dist/middleware/request-id.d.ts +10 -0
  39. package/dist/middleware/sentry-context.d.ts +8 -0
  40. package/dist/routes/activity-feed.d.ts +64 -0
  41. package/dist/routes/admin-bevakningar.d.ts +200 -0
  42. package/dist/routes/admin-companies.d.ts +381 -0
  43. package/dist/routes/admin-email-jobs.d.ts +257 -0
  44. package/dist/routes/admin-email-logs.d.ts +9 -0
  45. package/dist/routes/admin-fb-leads.d.ts +32 -0
  46. package/dist/routes/admin-import.d.ts +188 -0
  47. package/dist/routes/admin-login-history.d.ts +9 -0
  48. package/dist/routes/admin-marketing.d.ts +15 -0
  49. package/dist/routes/admin-metabase.d.ts +9 -0
  50. package/dist/routes/admin-notifications.d.ts +7 -0
  51. package/dist/routes/admin-paying-customers.d.ts +74 -0
  52. package/dist/routes/admin-sessions.d.ts +10 -0
  53. package/dist/routes/admin-stats.d.ts +380 -0
  54. package/dist/routes/admin-system-logs.d.ts +10 -0
  55. package/dist/routes/admin-users.d.ts +299 -0
  56. package/dist/routes/admin-webhooks.d.ts +276 -0
  57. package/dist/routes/api-keys.d.ts +123 -0
  58. package/dist/routes/applications.d.ts +385 -0
  59. package/dist/routes/auth.d.ts +15 -0
  60. package/dist/routes/billing.d.ts +369 -0
  61. package/dist/routes/bostadsmerit.d.ts +51 -0
  62. package/dist/routes/companies.d.ts +842 -0
  63. package/dist/routes/contact-reveals.d.ts +102 -0
  64. package/dist/routes/conversations/handlers/conversation.d.ts +5 -0
  65. package/dist/routes/conversations/handlers/initiate.d.ts +4 -0
  66. package/dist/routes/conversations/handlers/messages.d.ts +5 -0
  67. package/dist/routes/conversations/handlers/state.d.ts +5 -0
  68. package/dist/routes/conversations/helpers/access.d.ts +11 -0
  69. package/dist/routes/conversations/helpers/enrich-conversation.d.ts +58 -0
  70. package/dist/routes/conversations/helpers/identity.d.ts +43 -0
  71. package/dist/routes/conversations/helpers/notify-recipient.d.ts +10 -0
  72. package/dist/routes/conversations/helpers/reconcile-reveal.d.ts +10 -0
  73. package/dist/routes/conversations/helpers/scrub-contact.d.ts +1 -0
  74. package/dist/routes/conversations/index.d.ts +422 -0
  75. package/dist/routes/conversations/routes.d.ts +924 -0
  76. package/dist/routes/conversations/schemas.d.ts +216 -0
  77. package/dist/routes/cron.d.ts +27 -0
  78. package/dist/routes/documents.d.ts +493 -0
  79. package/dist/routes/email-actions.d.ts +8 -0
  80. package/dist/routes/fastighetslista.d.ts +94 -0
  81. package/dist/routes/geo.d.ts +518 -0
  82. package/dist/routes/geocoding.d.ts +192 -0
  83. package/dist/routes/health.d.ts +43 -0
  84. package/dist/routes/housing-history.d.ts +381 -0
  85. package/dist/routes/index.d.ts +15321 -0
  86. package/dist/routes/leads.d.ts +281 -0
  87. package/dist/routes/listing-helpers.d.ts +33 -0
  88. package/dist/routes/listing-publications.d.ts +636 -0
  89. package/dist/routes/listings.d.ts +1846 -0
  90. package/dist/routes/location-interests.d.ts +754 -0
  91. package/dist/routes/lookup.d.ts +109 -0
  92. package/dist/routes/mejl.d.ts +377 -0
  93. package/dist/routes/profiles.d.ts +281 -0
  94. package/dist/routes/properties.d.ts +1266 -0
  95. package/dist/routes/public-listings.d.ts +1137 -0
  96. package/dist/routes/public-profiles.d.ts +293 -0
  97. package/dist/routes/references.d.ts +695 -0
  98. package/dist/routes/search-partners.d.ts +4 -0
  99. package/dist/routes/site-config.d.ts +103 -0
  100. package/dist/routes/storage.d.ts +367 -0
  101. package/dist/routes/tenant-boost.d.ts +229 -0
  102. package/dist/routes/tenants.d.ts +336 -0
  103. package/dist/routes/track.d.ts +19 -0
  104. package/dist/routes/translate.d.ts +51 -0
  105. package/dist/routes/users.d.ts +517 -0
  106. package/dist/routes/verification.d.ts +175 -0
  107. package/dist/routes/webhooks.d.ts +9 -0
  108. package/dist/rpc.d.ts +11 -0
  109. package/dist/serve.d.ts +5 -0
  110. package/dist/services/activity-feed/activity-feed.service.d.ts +26 -0
  111. package/dist/services/applications/approval.service.d.ts +17 -0
  112. package/dist/services/auth/bankid-login.service.d.ts +40 -0
  113. package/dist/services/billing/constants.d.ts +2 -0
  114. package/dist/services/billing/contact-billing.service.d.ts +59 -0
  115. package/dist/services/billing/customer.service.d.ts +14 -0
  116. package/dist/services/billing/invoice-item.service.d.ts +49 -0
  117. package/dist/services/billing/invoice.service.d.ts +21 -0
  118. package/dist/services/billing/listing-upgrade-checkout.service.d.ts +45 -0
  119. package/dist/services/billing/purchase-receipt-email.d.ts +23 -0
  120. package/dist/services/billing/reveal-allowance.service.d.ts +33 -0
  121. package/dist/services/billing/stripe.d.ts +6 -0
  122. package/dist/services/billing/subscription.service.d.ts +21 -0
  123. package/dist/services/billing/types.d.ts +64 -0
  124. package/dist/services/billing/verify-session.service.d.ts +17 -0
  125. package/dist/services/billing/webhook.service.d.ts +8 -0
  126. package/dist/services/bostadsmerit/calculator.d.ts +51 -0
  127. package/dist/services/bostadsmerit/couple-calculator.d.ts +46 -0
  128. package/dist/services/bostadsmerit/tracker.service.d.ts +45 -0
  129. package/dist/services/chat-access/unlock-chat.service.d.ts +62 -0
  130. package/dist/services/conversations/upsert-conversation.d.ts +11 -0
  131. package/dist/services/email-jobs/email-job-sender.d.ts +7 -0
  132. package/dist/services/email-jobs/email-job.service.d.ts +67 -0
  133. package/dist/services/geo/bevakning-matching.service.d.ts +67 -0
  134. package/dist/services/geo/geo-listings.service.d.ts +38 -0
  135. package/dist/services/geo/geo.service.d.ts +233 -0
  136. package/dist/services/geo/geocode.service.d.ts +16 -0
  137. package/dist/services/geo/market-insights-by-coords.service.d.ts +67 -0
  138. package/dist/services/geo/market-insights.service.d.ts +44 -0
  139. package/dist/services/geo/market-overview.service.d.ts +42 -0
  140. package/dist/services/homii/image.d.ts +24 -0
  141. package/dist/services/homii/index.d.ts +12 -0
  142. package/dist/services/homii/location.d.ts +32 -0
  143. package/dist/services/homii/mapper.d.ts +41 -0
  144. package/dist/services/homii/types.d.ts +91 -0
  145. package/dist/services/leads/constants.d.ts +32 -0
  146. package/dist/services/leads/generate-leads.service.d.ts +38 -0
  147. package/dist/services/leads/matching.service.d.ts +55 -0
  148. package/dist/services/leads/tier.service.d.ts +6 -0
  149. package/dist/services/leads/types.d.ts +27 -0
  150. package/dist/services/listings/seo.service.d.ts +57 -0
  151. package/dist/services/listings/status.d.ts +37 -0
  152. package/dist/services/mejl/client.d.ts +38 -0
  153. package/dist/services/mrkoll/client.d.ts +95 -0
  154. package/dist/services/mrkoll/import.d.ts +38 -0
  155. package/dist/services/mrkoll/match.d.ts +35 -0
  156. package/dist/services/notifications/bibi-projects.d.ts +43 -0
  157. package/dist/services/notifications/bibi.d.ts +229 -0
  158. package/dist/services/profiles/bankid-verify.d.ts +23 -0
  159. package/dist/services/realtime.d.ts +14 -0
  160. package/dist/services/references/history-linker.d.ts +19 -0
  161. package/dist/services/tenant-boost/constants.d.ts +120 -0
  162. package/dist/services/tenant-boost/tenant-boost.service.d.ts +59 -0
  163. package/package.json +29 -0
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Homii posting orchestrator.
3
+ *
4
+ * Queries listing + property + images from DB, maps to Homii payload,
5
+ * resolves location, uploads image, and posts to Homii API.
6
+ *
7
+ * Returns a structured result with per-step status for admin visibility.
8
+ */
9
+ import type { HomiiPostResult, HomiiPreviewResult } from "./types";
10
+ export { type HomiiPostResult, type HomiiPreviewResult, type HomiiBroadcastMeta } from "./types";
11
+ export declare function previewHomiiListing(listingId: string): Promise<HomiiPreviewResult>;
12
+ export declare function postListingToHomii(listingId: string): Promise<HomiiPostResult>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Homii location resolver — resolves a city name to a Google Place ID
3
+ * using the Google Places Text Search API.
4
+ *
5
+ * Homii's own location-autocomplete API was deprecated (404 since 2025).
6
+ * Their post-listing endpoint requires a `locationGooglePlaceId` field.
7
+ *
8
+ * NOTE (2026-03-15): Only Stockholm-region listings return 204 from Homii.
9
+ * Göteborg, Malmö, Örebro, Borås, Uppsala all return 500 from Homii API.
10
+ */
11
+ export interface LocationResult {
12
+ success: boolean;
13
+ placeId?: string;
14
+ region?: string;
15
+ error?: string;
16
+ }
17
+ interface ResolveLocationOpts {
18
+ /** Street address (e.g. "Barkarbyvägen 30C") */
19
+ street?: string | null;
20
+ /** City name (e.g. "Järfälla") */
21
+ city: string;
22
+ }
23
+ /**
24
+ * Resolve an address to a Google Place ID via Google Places Text Search API.
25
+ *
26
+ * Uses street + city when available to get a locality-level place ID
27
+ * (not just a municipality). Homii extracts `locality` from the place ID
28
+ * for their page title — a municipality-level ID leaves `locality` null
29
+ * which causes "Ledig lägenhet att hyra - null" on their site.
30
+ */
31
+ export declare function resolveLocation(cityOrOpts: string | null | undefined | ResolveLocationOpts): Promise<LocationResult>;
32
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Homii payload mapper — converts Bofrid DB rows to Homii API payload.
3
+ */
4
+ import type { HomiiListingPayload } from "./types";
5
+ export interface PropertyData {
6
+ propertyType: string;
7
+ propertySize: number;
8
+ totalRooms: number;
9
+ description: string | null;
10
+ furnishingStatus: string | null;
11
+ amenities: string[] | null;
12
+ includedUtilities: string[] | null;
13
+ baseMonthlyRent: number | null;
14
+ rentalStart: string | null;
15
+ rentalEnd: string | null;
16
+ extensionPossible: boolean | null;
17
+ street: string;
18
+ streetNumber: string | null;
19
+ postalCode: string | null;
20
+ city: string;
21
+ }
22
+ export interface ImageData {
23
+ url: string;
24
+ urls: unknown;
25
+ isPrimary: boolean;
26
+ }
27
+ export interface MapperInput {
28
+ property: PropertyData;
29
+ images: ImageData[];
30
+ listingId: string;
31
+ publicUrl?: string | null;
32
+ }
33
+ export interface MapperResult {
34
+ success: boolean;
35
+ payload: HomiiListingPayload | null;
36
+ sourceImageUrl: string | null;
37
+ warnings: string[];
38
+ errors: string[];
39
+ }
40
+ export declare function buildAddress(p: PropertyData): string;
41
+ export declare function mapToHomiiPayload(input: MapperInput): MapperResult;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Homii API types — payload, response, and mapping types.
3
+ *
4
+ * Homii's post-listing API is unauthenticated (device_id cookie only).
5
+ * Endpoint: POST https://subletting.api.homii.se/post-listing
6
+ */
7
+ export type HomiiPropertyType = "APARTMENT" | "HOUSE" | "ROOM";
8
+ export type HomiiAvailabilityType = "IMMEDIATE" | "DETERMINED";
9
+ export type HomiiLeaseLengthType = "INDEFINITE" | "FIXED_TERM";
10
+ export interface HomiiListingPayload {
11
+ listingId: string;
12
+ renterFirstName: string;
13
+ renterLastName: string;
14
+ renterEmailAddress: string;
15
+ headline: string;
16
+ description: string;
17
+ pictureUrl: string;
18
+ propertyType: HomiiPropertyType;
19
+ propertyNumberOfRooms: number;
20
+ propertySquareMeters: number;
21
+ propertyPrivateEntrance: boolean;
22
+ propertyPrivateBathroom: boolean;
23
+ propertyPrivateKitchen: boolean;
24
+ locationGooglePlaceId: string;
25
+ leasePeriodAvailabilityType: HomiiAvailabilityType;
26
+ leasePeriodFromDate?: string;
27
+ leasePeriodLengthType: HomiiLeaseLengthType;
28
+ leasePeriodToDate?: string;
29
+ leasePeriodPossibleToExtend: boolean;
30
+ rentSpecificationMonthlyRent: number;
31
+ rentSpecificationCableTvIncluded: boolean;
32
+ rentSpecificationInternetIncluded: boolean;
33
+ rentSpecificationElectricityIncluded: boolean;
34
+ featuresAndAmenitiesFurnished: boolean;
35
+ featuresAndAmenitiesBalcony: boolean;
36
+ featuresAndAmenitiesWasher: boolean;
37
+ featuresAndAmenitiesDishwasher: boolean;
38
+ featuresAndAmenitiesBathtub: boolean;
39
+ featuresAndAmenitiesParking: boolean;
40
+ featuresAndAmenitiesElevator: boolean;
41
+ }
42
+ export interface HomiiPreviewResult {
43
+ canPost: boolean;
44
+ errors: string[];
45
+ warnings: string[];
46
+ payload: HomiiListingPayload | null;
47
+ sourceImageUrl: string | null;
48
+ address: string;
49
+ region: string | null;
50
+ }
51
+ export interface HomiiStepResult {
52
+ success: boolean;
53
+ error?: string;
54
+ detail?: string;
55
+ }
56
+ export interface HomiiPostResult {
57
+ success: boolean;
58
+ homiiListingId?: string;
59
+ homiiUrl?: string;
60
+ /** Raw response from Homii API (for debugging) */
61
+ homiiResponse?: unknown;
62
+ /** The full payload sent to Homii (for debugging) */
63
+ payload?: HomiiListingPayload;
64
+ error?: string;
65
+ warnings: string[];
66
+ steps: {
67
+ mapping: HomiiStepResult;
68
+ location: HomiiStepResult;
69
+ image: HomiiStepResult;
70
+ posting: HomiiStepResult;
71
+ };
72
+ }
73
+ export interface HomiiBroadcastMeta {
74
+ homiiListingId?: string;
75
+ homiiUrl?: string;
76
+ homiiResponse?: unknown;
77
+ /** The full Homii payload that was sent (for admin debugging) */
78
+ payload?: HomiiListingPayload;
79
+ /** Who triggered the posting */
80
+ triggeredBy?: {
81
+ name: string;
82
+ email?: string;
83
+ };
84
+ warnings?: string[];
85
+ steps?: {
86
+ mapping: HomiiStepResult;
87
+ location: HomiiStepResult;
88
+ image: HomiiStepResult;
89
+ posting: HomiiStepResult;
90
+ };
91
+ }
@@ -0,0 +1,32 @@
1
+ import type { LeadTier } from "./types";
2
+ export type UpgradeFeature = "unlimited_reveals" | "auto_conversations" | "bevakning_email" | "priority_visibility";
3
+ export interface UpgradeTierDef {
4
+ /** Tier version (1, 2, …). Stored in DB as `upgrade_tier`. */
5
+ tier: number;
6
+ /** Price in öre at time of sale */
7
+ priceOre: number;
8
+ /** Stripe product label (Swedish) */
9
+ label: string;
10
+ /** Features included in this tier */
11
+ features: readonly UpgradeFeature[];
12
+ }
13
+ /**
14
+ * All tier definitions, ordered by version.
15
+ * RULES:
16
+ * - NEVER remove or mutate an existing entry (old purchases reference it).
17
+ * - To change price or add features, append a new tier and bump CURRENT_UPGRADE_TIER.
18
+ */
19
+ export declare const UPGRADE_TIERS: readonly UpgradeTierDef[];
20
+ /** The tier sold to new buyers right now. Bump this when adding a new tier. */
21
+ export declare const CURRENT_UPGRADE_TIER = 2;
22
+ /** Helper: get tier definition by version number */
23
+ export declare function getUpgradeTier(tier: number): UpgradeTierDef | undefined;
24
+ /** Helper: get the current tier definition (what new buyers get) */
25
+ export declare function getCurrentUpgradeTier(): UpgradeTierDef;
26
+ /** Helper: check if a tier includes a specific feature */
27
+ export declare function tierHasFeature(tier: number | null | undefined, feature: UpgradeFeature): boolean;
28
+ /** @deprecated Use getCurrentUpgradeTier().priceOre instead */
29
+ export declare const LISTING_UPGRADE_PRICE_ORE = 50000;
30
+ export declare const LEAD_TIER_SCORES: Record<LeadTier, number>;
31
+ export declare const MIN_KEY_DOC_CATEGORIES = 2;
32
+ export declare const KEY_DOC_CATEGORIES: readonly ["credit_report", "criminal_record_extract", "income_doc"];
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Lead Generation Service
3
+ *
4
+ * Generates leads by matching bevakningar to published listings.
5
+ * Supports both directions:
6
+ * - Forward: listing publishes → find matching bevakningar → create leads
7
+ * - Reverse: bevakning created → find matching published listings → create leads
8
+ */
9
+ export interface GenerateLeadsForListingOpts {
10
+ listingId: string;
11
+ /** The landlord/admin user triggering this */
12
+ actingUserId: string;
13
+ }
14
+ export declare function generateLeadsForListing(opts: GenerateLeadsForListingOpts): Promise<{
15
+ created: number;
16
+ skipped: number;
17
+ errors: number;
18
+ }>;
19
+ export interface GenerateLeadsForBevakningOpts {
20
+ bevakningId: string;
21
+ userId: string;
22
+ filters: {
23
+ maxRent?: number;
24
+ propertyTypes?: string[];
25
+ minRooms?: number;
26
+ maxRooms?: number;
27
+ } | null;
28
+ numberOfPeople?: number | null;
29
+ }
30
+ /**
31
+ * When a new bevakning is created, find all published listings whose property
32
+ * coordinates fall within the bevakning's polygon and create leads.
33
+ */
34
+ export declare function generateLeadsForBevakning(opts: GenerateLeadsForBevakningOpts): Promise<{
35
+ created: number;
36
+ skipped: number;
37
+ errors: number;
38
+ }>;
@@ -0,0 +1,55 @@
1
+ import type { MatchDetails } from "./types";
2
+ export interface LocationInterest {
3
+ id: string;
4
+ userId: string;
5
+ active: boolean;
6
+ emailNotifications: boolean;
7
+ location: {
8
+ place_id?: string;
9
+ name?: string;
10
+ bounds?: {
11
+ southwest: {
12
+ lat: number;
13
+ lng: number;
14
+ };
15
+ northeast: {
16
+ lat: number;
17
+ lng: number;
18
+ };
19
+ };
20
+ };
21
+ numberOfPeople?: number;
22
+ }
23
+ export interface ListingForMatching {
24
+ id: string;
25
+ location: {
26
+ geoCoordinates: {
27
+ lat: number;
28
+ lon: number;
29
+ };
30
+ };
31
+ rules: {
32
+ maxOccupancy: number;
33
+ };
34
+ }
35
+ export interface BevakningForMatching extends LocationInterest {
36
+ filters?: {
37
+ maxRent?: number;
38
+ propertyTypes?: string[];
39
+ minRooms?: number;
40
+ maxRooms?: number;
41
+ };
42
+ }
43
+ export interface ListingForLeadMatching extends ListingForMatching {
44
+ pricing: {
45
+ baseMonthlyRent: number;
46
+ };
47
+ listing: {
48
+ propertyType: string;
49
+ };
50
+ dimensions: {
51
+ totalRooms: number;
52
+ };
53
+ landlordId: string;
54
+ }
55
+ export declare function matchBevakningToListing(bevakning: BevakningForMatching, listing: ListingForLeadMatching): MatchDetails | null;
@@ -0,0 +1,6 @@
1
+ import type { LeadSource, LeadTier } from "./types";
2
+ export declare function classifyTier(source: LeadSource, hasUploadedDocs: boolean): LeadTier;
3
+ export declare function getTierScore(tier: LeadTier): number;
4
+ export declare function hasKeyDocsUploaded(documents: Array<{
5
+ category?: string;
6
+ }> | undefined): boolean;
@@ -0,0 +1,27 @@
1
+ export type LeadTier = "super_premium" | "premium" | "standard" | "basic";
2
+ export type LeadSource = "bevakning" | "application" | "both";
3
+ export type LeadStatus = "active" | "expired" | "converted" | "declined";
4
+ export interface MaskedTenantData {
5
+ firstInitial: string;
6
+ age?: number;
7
+ bostadsmeritScore: number;
8
+ employmentStatus?: string;
9
+ hasVerifiedDocs: boolean;
10
+ verifiedDocCount: number;
11
+ uploadedDocCount: number;
12
+ referenceCount: number;
13
+ numberOfPeople?: number;
14
+ firstName?: string;
15
+ title?: string;
16
+ profilePictureUrl?: string;
17
+ email?: string;
18
+ phoneNumber?: string;
19
+ about?: string;
20
+ }
21
+ export interface MatchDetails {
22
+ geographicMatch: boolean;
23
+ rentInBudget: boolean;
24
+ propertyTypeMatch: boolean;
25
+ roomCountMatch: boolean;
26
+ overallMatchScore: number;
27
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * SEO URL Generation Service
3
+ *
4
+ * Generates listing SEO URLs server-side during listing creation.
5
+ * Uses the Bofrid Geo API (SCB polygon data) for reverse geocoding,
6
+ * then builds deterministic URL slugs from property data.
7
+ *
8
+ * URL Pattern (Swedish): /hyra-{type}/{län}/{kommun}/{område}/{slug}
9
+ * URL Pattern (English): /en/rent-{type}/sweden/{län}/{kommun}/{område}/{slug}
10
+ * Slug format: {street}-{rooms}-{size}kvm-{type}-{timestamp}
11
+ */
12
+ interface SEOInput {
13
+ propertyType: string;
14
+ totalRooms: number;
15
+ propertySize: number;
16
+ street: string;
17
+ streetNumber?: string | null;
18
+ city?: string | null;
19
+ municipality?: string | null;
20
+ lat: number;
21
+ lon: number;
22
+ shortId: string;
23
+ }
24
+ interface SEOResult {
25
+ slug: string;
26
+ regionSlug: string;
27
+ municipalitySlug: string;
28
+ områdeSlug: string;
29
+ isCardinalFallback: boolean;
30
+ routePrefix: "hyra-lagenhet" | "hyra-hus";
31
+ fullPath: string;
32
+ fullPathEn: string;
33
+ }
34
+ export declare function generateListingSEO(input: SEOInput): Promise<SEOResult | null>;
35
+ export declare function generateAndSaveListingSEO(listingId: string, input: SEOInput): Promise<void>;
36
+ export declare function ensureListingSEO(listingId: string): Promise<void>;
37
+ /** Fields that affect the SEO URL and should trigger regeneration */
38
+ export declare const SEO_RELEVANT_FIELDS: Set<string>;
39
+ export interface PropertyGeo {
40
+ geoLan: string;
41
+ geoKommun: string;
42
+ geoOmrade: string;
43
+ geoRoutePrefix: string;
44
+ }
45
+ /**
46
+ * Reverse geocode lat/lon and save geo columns on the property.
47
+ * Returns the resolved geo data, or null if reverse geocode fails.
48
+ */
49
+ export declare function resolveAndSavePropertyGeo(propertyId: string, lat: number, lon: number, propertyType: string): Promise<PropertyGeo | null>;
50
+ /**
51
+ * Refresh SEO URLs for all active listings of a property.
52
+ * Also updates the property's geo columns.
53
+ * Called when SEO-relevant property fields change (address, type, size, rooms).
54
+ * Fire-and-forget safe — logs errors but does not throw.
55
+ */
56
+ export declare function refreshPropertyListingSEO(propertyId: string): Promise<void>;
57
+ export {};
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Listing status taxonomy and visibility predicates.
3
+ *
4
+ * `bofrid_listings.status` is plain text in the schema. The valid values and
5
+ * the questions we ask about them ("does this still block the property slot?",
6
+ * "does it count against the user's quota?") live here so they live in one
7
+ * place instead of being re-spelled at every call site.
8
+ *
9
+ * Each `where*` helper returns a Drizzle SQL predicate suitable for
10
+ * `where(and(..., wherePredicate()))`. They reference `listings.status`
11
+ * directly, so they only work in queries where `listings` is in scope.
12
+ *
13
+ * Note: `eq(listings.status, "published")` is intentionally NOT wrapped here —
14
+ * call sites differ on whether they also require `publishedAt IS NOT NULL`,
15
+ * and unifying them is a separate question worth its own pass.
16
+ */
17
+ import type { SQL } from "drizzle-orm";
18
+ export declare const LISTING_STATUSES: readonly ["draft", "under_review", "rejected", "published", "archived", "rented", "deleted"];
19
+ export type ListingStatus = (typeof LISTING_STATUSES)[number];
20
+ /**
21
+ * Listing is not soft-deleted — safe for owner/admin reads.
22
+ */
23
+ export declare const whereNotDeleted: () => SQL;
24
+ /**
25
+ * Listing occupies the property's "advertised" slot — another listing on the
26
+ * same property cannot be created while one of these exists. Mirrors the
27
+ * partial unique index `uq_listings_one_active_per_property`
28
+ * (`status NOT IN ('archived','rented','deleted')`). Rented listings free the
29
+ * slot because the rental cycle is complete.
30
+ */
31
+ export declare const whereOccupyingPropertySlot: () => SQL;
32
+ /**
33
+ * Listing counts toward the landlord's MAX_DRAFTS quota. Wider than
34
+ * `whereOccupyingPropertySlot` — `rented` listings still count, since they
35
+ * remain on the landlord's roster until archived.
36
+ */
37
+ export declare const whereCountedTowardQuota: () => SQL;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * mejl.to (MailMCP) HTTP client.
3
+ * Docs: https://docs.mejl.to — base URL: https://api.mejl.to
4
+ */
5
+ export declare class MejlConfigError extends Error {
6
+ }
7
+ export declare class MejlApiError extends Error {
8
+ status: number;
9
+ body: unknown;
10
+ constructor(message: string, status: number, body: unknown);
11
+ }
12
+ export declare const mejl: {
13
+ listInboxes: (q: {
14
+ limit?: number;
15
+ page_token?: string;
16
+ }) => Promise<unknown>;
17
+ listMessages: (inboxId: string, q: {
18
+ limit?: number;
19
+ page_token?: string;
20
+ labels?: string;
21
+ before?: string;
22
+ after?: string;
23
+ ascending?: boolean;
24
+ include_spam?: boolean;
25
+ }) => Promise<unknown>;
26
+ getMessage: (inboxId: string, messageId: string) => Promise<unknown>;
27
+ sendMessage: (inboxId: string, body: Record<string, unknown>) => Promise<unknown>;
28
+ sendSms: (body: {
29
+ to: string | string[];
30
+ text: string;
31
+ ttl?: number;
32
+ with_delivery_report?: boolean;
33
+ }) => Promise<unknown>;
34
+ listSmsMessages: (q: {
35
+ limit?: number;
36
+ }) => Promise<unknown>;
37
+ getSmsMessage: (id: string) => Promise<unknown>;
38
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * mrkoll chromebox client.
3
+ *
4
+ * Talks to the chromebox /exec endpoint that drives the `mrkoll` Chromium
5
+ * profile. Read-only — only `search-person` and `get-person`.
6
+ *
7
+ * Profile isolation: every call passes `--account <MRKOLL_ACCOUNT>` (default
8
+ * "mrkoll"). The chromebox maps each account slug to its own Chromium
9
+ * `user_data_dir` (verify via GET /chrome/profiles), so cookies, Cloudflare
10
+ * clearance and IAB consent persist independently of other adapters
11
+ * (facebook, strato, devto, …) running on the same chromebox container.
12
+ * Never point MRKOLL_ACCOUNT at "default" — that's the shared profile.
13
+ */
14
+ export interface MrkollSearchHit {
15
+ id: string;
16
+ person_url: string;
17
+ full_name: string;
18
+ age: number | null;
19
+ street_address: string | null;
20
+ postal_code: string | null;
21
+ city: string | null;
22
+ pnr_prefix: string | null;
23
+ resident_since: string | null;
24
+ }
25
+ export interface MrkollSearchResult {
26
+ ok: boolean;
27
+ query: string;
28
+ location: string | null;
29
+ total: number;
30
+ pnr_filtered_total: number;
31
+ pnr_filter_applied: string | null;
32
+ results: MrkollSearchHit[];
33
+ aborted_reason: string | null;
34
+ }
35
+ export interface MrkollPreviousAddress {
36
+ street: string;
37
+ postal_code: string | null;
38
+ city: string | null;
39
+ effective: string;
40
+ }
41
+ export interface MrkollHouseholdMember {
42
+ name: string;
43
+ person_url: string;
44
+ }
45
+ export interface MrkollPerson {
46
+ ok: boolean;
47
+ id: string;
48
+ person_url: string;
49
+ display_name: string | null;
50
+ calling_name: string | null;
51
+ middle_name: string | null;
52
+ last_name: string | null;
53
+ age: number | null;
54
+ gender: string | null;
55
+ birth_date: string | null;
56
+ pnr_full: string | null;
57
+ street_address: string | null;
58
+ postal_code: string | null;
59
+ city: string | null;
60
+ municipality: string | null;
61
+ region: string | null;
62
+ housing_type: string | null;
63
+ lives_alone: boolean | null;
64
+ name_day: string | null;
65
+ household_members: MrkollHouseholdMember[];
66
+ previous_addresses: MrkollPreviousAddress[];
67
+ name_changes?: {
68
+ first?: boolean;
69
+ middle?: boolean;
70
+ last_was?: string | null;
71
+ } | null;
72
+ phone: string | null;
73
+ vehicle_summary: string | null;
74
+ area_avg_income: number | null;
75
+ area_max_income: number | null;
76
+ income_indicator: number | null;
77
+ has_dogs: boolean | null;
78
+ company_engagements: string | null;
79
+ fetched_at: string | null;
80
+ aborted_reason: string | null;
81
+ }
82
+ export declare class MrkollClientError extends Error {
83
+ readonly kind: "not_configured" | "network" | "http_error" | "exec_error" | "timeout";
84
+ readonly status?: number | undefined;
85
+ constructor(message: string, kind: "not_configured" | "network" | "http_error" | "exec_error" | "timeout", status?: number | undefined);
86
+ }
87
+ export interface SearchPersonInput {
88
+ query: string;
89
+ pnr?: string;
90
+ location?: string;
91
+ limit?: number;
92
+ }
93
+ export declare function searchPerson(input: SearchPersonInput): Promise<MrkollSearchResult>;
94
+ export declare function getPerson(id: string): Promise<MrkollPerson>;
95
+ export declare function isMrkollConfigured(): boolean;
@@ -0,0 +1,38 @@
1
+ import { type MrkollPerson } from "./client";
2
+ import { type HousingHistoryInsert } from "./match";
3
+ export type MrkollImportResult = {
4
+ status: "verified";
5
+ entries: HousingHistoryInsert[];
6
+ person: MrkollPerson;
7
+ } | {
8
+ status: "not_found";
9
+ } | {
10
+ status: "ambiguous";
11
+ hitCount: number;
12
+ } | {
13
+ status: "pnr_mismatch";
14
+ } | {
15
+ status: "rate_limited";
16
+ } | {
17
+ status: "error";
18
+ reason: string;
19
+ };
20
+ export interface MrkollImportInput {
21
+ profileId: string;
22
+ fullName: string;
23
+ personnummer: string;
24
+ }
25
+ /**
26
+ * Resolve a Bofrid profile to its mrkoll record and produce verified
27
+ * housing-history rows. Read-only against mrkoll, server-side only.
28
+ *
29
+ * Match path:
30
+ * 1. search-person --query <fullName> --pnr <pnr>
31
+ * 2. on 0 hits -> not_found
32
+ * 3. on 1 hit -> get-person -> assert pnr_full
33
+ * 4. on 2+ hits -> get-person on each, look for exactly one pnr_full match
34
+ *
35
+ * The 100% certainty step is `pnr_full` equality after `get-person`.
36
+ * `pnr_full` is consumed in-memory only and never logged or persisted.
37
+ */
38
+ export declare function importMrkollFolkbokforing(input: MrkollImportInput): Promise<MrkollImportResult>;
@@ -0,0 +1,35 @@
1
+ import type { MrkollPerson } from "./client";
2
+ import type { housingHistory } from "@bofrid/db";
3
+ export type HousingHistoryInsert = typeof housingHistory.$inferInsert;
4
+ /**
5
+ * Parse mrkoll's Swedish "effective" string for a previous address.
6
+ * - "Flyttade till denna tidigare adress 2021-06-01" -> "2021-06-01"
7
+ * - "Flyttdatumet till denna tidigare adress är okänt" -> null
8
+ * - anything else -> null
9
+ */
10
+ export declare function parsePnrEffective(effective: string | null | undefined): {
11
+ moveInDate: string | null;
12
+ };
13
+ /**
14
+ * Strip non-digits from a personnummer.
15
+ */
16
+ export declare function normalizeDigits(input: string | null | undefined): string;
17
+ /**
18
+ * Redact a personnummer to "YYYYMMDD-XXXX" form for safe logging.
19
+ * Anything we can't recognise -> "redacted".
20
+ */
21
+ export declare function redactPnr(input: string | null | undefined): string;
22
+ /**
23
+ * Compare two personnummer for equality after normalising. Both must be
24
+ * 12 digits or this returns false.
25
+ */
26
+ export declare function pnrMatches(expected: string, got: string | null | undefined): boolean;
27
+ /**
28
+ * Map a verified mrkoll person record into housing_history insert rows.
29
+ * Marks rows as `verified: true, verifiedAt: now()` (verifiedBy stays null
30
+ * — system, not an admin).
31
+ *
32
+ * `moveOutDate` is the next-newer move-in date, mirroring the existing
33
+ * Nusvar logic: previous_addresses[] are sorted newest-first.
34
+ */
35
+ export declare function mrkollToHousingEntries(person: MrkollPerson, profileId: string): HousingHistoryInsert[];