@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.
- package/README.md +24 -0
- package/dist/app.d.ts +23 -0
- package/dist/dev.d.ts +6 -0
- package/dist/export-openapi.d.ts +9 -0
- package/dist/index.d.ts +3 -0
- package/dist/lib/auth.d.ts +20 -0
- package/dist/lib/criipto-bankid.d.ts +45 -0
- package/dist/lib/datalake.d.ts +7 -0
- package/dist/lib/docs-filter.d.ts +15 -0
- package/dist/lib/email-action-token.d.ts +26 -0
- package/dist/lib/email-utm.d.ts +42 -0
- package/dist/lib/email.d.ts +94 -0
- package/dist/lib/env.d.ts +18 -0
- package/dist/lib/errors.d.ts +6 -0
- package/dist/lib/helpers.d.ts +6 -0
- package/dist/lib/logger.d.ts +25 -0
- package/dist/lib/markets.d.ts +1 -0
- package/dist/lib/org-number.d.ts +5 -0
- package/dist/lib/ownership.d.ts +47 -0
- package/dist/lib/pdf-watermark.d.ts +25 -0
- package/dist/lib/personnummer.d.ts +14 -0
- package/dist/lib/posthog.d.ts +51 -0
- package/dist/lib/premium.d.ts +13 -0
- package/dist/lib/profile-resolver.d.ts +14 -0
- package/dist/lib/redirect-state.d.ts +40 -0
- package/dist/lib/revalidate.d.ts +23 -0
- package/dist/lib/schemas.d.ts +21 -0
- package/dist/lib/sentry.d.ts +31 -0
- package/dist/lib/slug.d.ts +5 -0
- package/dist/lib/sms.d.ts +22 -0
- package/dist/lib/system-log.d.ts +31 -0
- package/dist/lib/webhook-events.d.ts +26 -0
- package/dist/lib/webhooks.d.ts +28 -0
- package/dist/middleware/auth-debug.d.ts +17 -0
- package/dist/middleware/auth.d.ts +19 -0
- package/dist/middleware/bibi-logger.d.ts +6 -0
- package/dist/middleware/cors.d.ts +1 -0
- package/dist/middleware/request-id.d.ts +10 -0
- package/dist/middleware/sentry-context.d.ts +8 -0
- package/dist/routes/activity-feed.d.ts +64 -0
- package/dist/routes/admin-bevakningar.d.ts +200 -0
- package/dist/routes/admin-companies.d.ts +381 -0
- package/dist/routes/admin-email-jobs.d.ts +257 -0
- package/dist/routes/admin-email-logs.d.ts +9 -0
- package/dist/routes/admin-fb-leads.d.ts +32 -0
- package/dist/routes/admin-import.d.ts +188 -0
- package/dist/routes/admin-login-history.d.ts +9 -0
- package/dist/routes/admin-marketing.d.ts +15 -0
- package/dist/routes/admin-metabase.d.ts +9 -0
- package/dist/routes/admin-notifications.d.ts +7 -0
- package/dist/routes/admin-paying-customers.d.ts +74 -0
- package/dist/routes/admin-sessions.d.ts +10 -0
- package/dist/routes/admin-stats.d.ts +380 -0
- package/dist/routes/admin-system-logs.d.ts +10 -0
- package/dist/routes/admin-users.d.ts +299 -0
- package/dist/routes/admin-webhooks.d.ts +276 -0
- package/dist/routes/api-keys.d.ts +123 -0
- package/dist/routes/applications.d.ts +385 -0
- package/dist/routes/auth.d.ts +15 -0
- package/dist/routes/billing.d.ts +369 -0
- package/dist/routes/bostadsmerit.d.ts +51 -0
- package/dist/routes/companies.d.ts +842 -0
- package/dist/routes/contact-reveals.d.ts +102 -0
- package/dist/routes/conversations/handlers/conversation.d.ts +5 -0
- package/dist/routes/conversations/handlers/initiate.d.ts +4 -0
- package/dist/routes/conversations/handlers/messages.d.ts +5 -0
- package/dist/routes/conversations/handlers/state.d.ts +5 -0
- package/dist/routes/conversations/helpers/access.d.ts +11 -0
- package/dist/routes/conversations/helpers/enrich-conversation.d.ts +58 -0
- package/dist/routes/conversations/helpers/identity.d.ts +43 -0
- package/dist/routes/conversations/helpers/notify-recipient.d.ts +10 -0
- package/dist/routes/conversations/helpers/reconcile-reveal.d.ts +10 -0
- package/dist/routes/conversations/helpers/scrub-contact.d.ts +1 -0
- package/dist/routes/conversations/index.d.ts +422 -0
- package/dist/routes/conversations/routes.d.ts +924 -0
- package/dist/routes/conversations/schemas.d.ts +216 -0
- package/dist/routes/cron.d.ts +27 -0
- package/dist/routes/documents.d.ts +493 -0
- package/dist/routes/email-actions.d.ts +8 -0
- package/dist/routes/fastighetslista.d.ts +94 -0
- package/dist/routes/geo.d.ts +518 -0
- package/dist/routes/geocoding.d.ts +192 -0
- package/dist/routes/health.d.ts +43 -0
- package/dist/routes/housing-history.d.ts +381 -0
- package/dist/routes/index.d.ts +15321 -0
- package/dist/routes/leads.d.ts +281 -0
- package/dist/routes/listing-helpers.d.ts +33 -0
- package/dist/routes/listing-publications.d.ts +636 -0
- package/dist/routes/listings.d.ts +1846 -0
- package/dist/routes/location-interests.d.ts +754 -0
- package/dist/routes/lookup.d.ts +109 -0
- package/dist/routes/mejl.d.ts +377 -0
- package/dist/routes/profiles.d.ts +281 -0
- package/dist/routes/properties.d.ts +1266 -0
- package/dist/routes/public-listings.d.ts +1137 -0
- package/dist/routes/public-profiles.d.ts +293 -0
- package/dist/routes/references.d.ts +695 -0
- package/dist/routes/search-partners.d.ts +4 -0
- package/dist/routes/site-config.d.ts +103 -0
- package/dist/routes/storage.d.ts +367 -0
- package/dist/routes/tenant-boost.d.ts +229 -0
- package/dist/routes/tenants.d.ts +336 -0
- package/dist/routes/track.d.ts +19 -0
- package/dist/routes/translate.d.ts +51 -0
- package/dist/routes/users.d.ts +517 -0
- package/dist/routes/verification.d.ts +175 -0
- package/dist/routes/webhooks.d.ts +9 -0
- package/dist/rpc.d.ts +11 -0
- package/dist/serve.d.ts +5 -0
- package/dist/services/activity-feed/activity-feed.service.d.ts +26 -0
- package/dist/services/applications/approval.service.d.ts +17 -0
- package/dist/services/auth/bankid-login.service.d.ts +40 -0
- package/dist/services/billing/constants.d.ts +2 -0
- package/dist/services/billing/contact-billing.service.d.ts +59 -0
- package/dist/services/billing/customer.service.d.ts +14 -0
- package/dist/services/billing/invoice-item.service.d.ts +49 -0
- package/dist/services/billing/invoice.service.d.ts +21 -0
- package/dist/services/billing/listing-upgrade-checkout.service.d.ts +45 -0
- package/dist/services/billing/purchase-receipt-email.d.ts +23 -0
- package/dist/services/billing/reveal-allowance.service.d.ts +33 -0
- package/dist/services/billing/stripe.d.ts +6 -0
- package/dist/services/billing/subscription.service.d.ts +21 -0
- package/dist/services/billing/types.d.ts +64 -0
- package/dist/services/billing/verify-session.service.d.ts +17 -0
- package/dist/services/billing/webhook.service.d.ts +8 -0
- package/dist/services/bostadsmerit/calculator.d.ts +51 -0
- package/dist/services/bostadsmerit/couple-calculator.d.ts +46 -0
- package/dist/services/bostadsmerit/tracker.service.d.ts +45 -0
- package/dist/services/chat-access/unlock-chat.service.d.ts +62 -0
- package/dist/services/conversations/upsert-conversation.d.ts +11 -0
- package/dist/services/email-jobs/email-job-sender.d.ts +7 -0
- package/dist/services/email-jobs/email-job.service.d.ts +67 -0
- package/dist/services/geo/bevakning-matching.service.d.ts +67 -0
- package/dist/services/geo/geo-listings.service.d.ts +38 -0
- package/dist/services/geo/geo.service.d.ts +233 -0
- package/dist/services/geo/geocode.service.d.ts +16 -0
- package/dist/services/geo/market-insights-by-coords.service.d.ts +67 -0
- package/dist/services/geo/market-insights.service.d.ts +44 -0
- package/dist/services/geo/market-overview.service.d.ts +42 -0
- package/dist/services/homii/image.d.ts +24 -0
- package/dist/services/homii/index.d.ts +12 -0
- package/dist/services/homii/location.d.ts +32 -0
- package/dist/services/homii/mapper.d.ts +41 -0
- package/dist/services/homii/types.d.ts +91 -0
- package/dist/services/leads/constants.d.ts +32 -0
- package/dist/services/leads/generate-leads.service.d.ts +38 -0
- package/dist/services/leads/matching.service.d.ts +55 -0
- package/dist/services/leads/tier.service.d.ts +6 -0
- package/dist/services/leads/types.d.ts +27 -0
- package/dist/services/listings/seo.service.d.ts +57 -0
- package/dist/services/listings/status.d.ts +37 -0
- package/dist/services/mejl/client.d.ts +38 -0
- package/dist/services/mrkoll/client.d.ts +95 -0
- package/dist/services/mrkoll/import.d.ts +38 -0
- package/dist/services/mrkoll/match.d.ts +35 -0
- package/dist/services/notifications/bibi-projects.d.ts +43 -0
- package/dist/services/notifications/bibi.d.ts +229 -0
- package/dist/services/profiles/bankid-verify.d.ts +23 -0
- package/dist/services/realtime.d.ts +14 -0
- package/dist/services/references/history-linker.d.ts +19 -0
- package/dist/services/tenant-boost/constants.d.ts +120 -0
- package/dist/services/tenant-boost/tenant-boost.service.d.ts +59 -0
- package/package.json +29 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-Access Service — single entry point for unlocking the chat between
|
|
3
|
+
* a landlord and a tenant on a given listing.
|
|
4
|
+
*
|
|
5
|
+
* The same effect is reachable through several paths:
|
|
6
|
+
* - landlord spends a free monthly reveal ('quota')
|
|
7
|
+
* - the listing is upgraded to premium, granting unlimited unlocks
|
|
8
|
+
* ('listing_premium')
|
|
9
|
+
* - the tenant subscribes to Boost, which unlocks every existing
|
|
10
|
+
* conversation involving them ('tenant_boost')
|
|
11
|
+
* - admin / share-with-tenant flows ('admin')
|
|
12
|
+
*
|
|
13
|
+
* Previously each path duplicated three writes (applications.revealedAt,
|
|
14
|
+
* leads.revealed/revealedAt, conversations.isLocked) and skipped the unlock
|
|
15
|
+
* reason, which caused Boost-triggered unlocks to silently consume the
|
|
16
|
+
* landlord's free quota at counting time. This module centralises the
|
|
17
|
+
* three-way sync and tags every row with its origin so only 'quota' unlocks
|
|
18
|
+
* count against the monthly budget.
|
|
19
|
+
*/
|
|
20
|
+
export type UnlockReason = "quota" | "listing_premium" | "tenant_boost" | "admin";
|
|
21
|
+
interface UnlockChatParams {
|
|
22
|
+
listingId: string;
|
|
23
|
+
tenantId: string;
|
|
24
|
+
landlordId: string;
|
|
25
|
+
reason: UnlockReason;
|
|
26
|
+
partnerId?: string | null;
|
|
27
|
+
}
|
|
28
|
+
interface UnlockChatResult {
|
|
29
|
+
conversationId: string;
|
|
30
|
+
/**
|
|
31
|
+
* True when this call flipped at least one application or lead row from
|
|
32
|
+
* unrevealed to revealed. Callers can use it to drive analytics / webhooks
|
|
33
|
+
* without double-firing on idempotent retries.
|
|
34
|
+
*/
|
|
35
|
+
changed: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Atomically unlock the chat between (listingId, tenantId, landlordId).
|
|
39
|
+
*
|
|
40
|
+
* Idempotent: callable multiple times for the same triple with no extra
|
|
41
|
+
* writes once the unlock has happened.
|
|
42
|
+
*/
|
|
43
|
+
export declare function unlockChat(params: UnlockChatParams): Promise<UnlockChatResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Batch-unlock every (tenant, listing) pair where a locked conversation
|
|
46
|
+
* exists for the given listing. Used by the listing-upgrade webhook.
|
|
47
|
+
*
|
|
48
|
+
* Returns the number of conversations that were unlocked (i.e. count of
|
|
49
|
+
* tenants whose chat moved from locked → unlocked).
|
|
50
|
+
*/
|
|
51
|
+
export declare function unlockAllChatsForListing(listingId: string, landlordId: string): Promise<number>;
|
|
52
|
+
/**
|
|
53
|
+
* Batch-unlock every locked conversation that involves the given tenant.
|
|
54
|
+
* Used by the Boost subscription activation hook.
|
|
55
|
+
*
|
|
56
|
+
* Each unlocked row is tagged `tenant_boost` so it never consumes a
|
|
57
|
+
* landlord's free monthly reveal quota.
|
|
58
|
+
*
|
|
59
|
+
* Returns the number of conversations that flipped from locked → unlocked.
|
|
60
|
+
*/
|
|
61
|
+
export declare function unlockAllChatsForTenant(tenantId: string): Promise<number>;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create or find a conversation for a tenant+listing pair (or couple+listing).
|
|
3
|
+
* Adds all participants if creating new.
|
|
4
|
+
* Returns the conversation ID.
|
|
5
|
+
*
|
|
6
|
+
* @param partnerId — If set, creates a couple/group conversation. All active
|
|
7
|
+
* partner members are added as participants alongside the landlord.
|
|
8
|
+
*/
|
|
9
|
+
export declare function upsertConversation(listingId: string, tenantId: string, landlordId: string, partnerId?: string | null, opts?: {
|
|
10
|
+
locked?: boolean;
|
|
11
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Job Sender — Renders and sends emails based on job type.
|
|
3
|
+
*
|
|
4
|
+
* Maps job types to their template renderers + email transport.
|
|
5
|
+
* Called by the cron worker via processNextBatch().
|
|
6
|
+
*/
|
|
7
|
+
export declare function sendEmailJobItem(jobPayload: Record<string, unknown>, itemPayload: Record<string, unknown>, recipientEmail: string, recipientUserId: string | null): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Job Service — Persistent queue for bulk email sends.
|
|
3
|
+
*
|
|
4
|
+
* Design constraints:
|
|
5
|
+
* - Multiple jobs can be queued; cron processes one at a time (FIFO by createdAt)
|
|
6
|
+
* - Cron worker picks up pending items in batches of 50
|
|
7
|
+
* - Failed items retry up to 3 times before marking as failed
|
|
8
|
+
* - Jobs can be cancelled mid-flight (remaining pending items get cancelled)
|
|
9
|
+
* - Vercel cron runs every 1 minute, processing fits within 60s timeout
|
|
10
|
+
*/
|
|
11
|
+
export type EmailJobType = "bevakning_match" | "verification_reminder" | "profile_reminder";
|
|
12
|
+
export interface CreateEmailJobInput {
|
|
13
|
+
type: EmailJobType;
|
|
14
|
+
referenceId?: string;
|
|
15
|
+
payload: Record<string, unknown>;
|
|
16
|
+
createdBy: string;
|
|
17
|
+
items: Array<{
|
|
18
|
+
recipientEmail: string;
|
|
19
|
+
recipientUserId?: string;
|
|
20
|
+
itemPayload: Record<string, unknown>;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export interface EmailJobStatus {
|
|
24
|
+
id: string;
|
|
25
|
+
type: string;
|
|
26
|
+
status: string;
|
|
27
|
+
referenceId: string | null;
|
|
28
|
+
referenceLabel: string | null;
|
|
29
|
+
totalItems: number;
|
|
30
|
+
sentCount: number;
|
|
31
|
+
failedCount: number;
|
|
32
|
+
pendingCount: number;
|
|
33
|
+
cancelledCount: number;
|
|
34
|
+
createdBy: string | null;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
startedAt: string | null;
|
|
37
|
+
completedAt: string | null;
|
|
38
|
+
cancelledAt: string | null;
|
|
39
|
+
}
|
|
40
|
+
export declare function getActiveJob(): Promise<{
|
|
41
|
+
id: string;
|
|
42
|
+
type: string;
|
|
43
|
+
status: string;
|
|
44
|
+
} | null>;
|
|
45
|
+
export declare function createEmailJob(input: CreateEmailJobInput): Promise<{
|
|
46
|
+
jobId: string;
|
|
47
|
+
totalItems: number;
|
|
48
|
+
}>;
|
|
49
|
+
export declare function cancelEmailJob(jobId: string): Promise<EmailJobStatus>;
|
|
50
|
+
export declare function getJobStatus(jobId: string): Promise<EmailJobStatus>;
|
|
51
|
+
export declare function listEmailJobs(limit?: number, referenceId?: string): Promise<EmailJobStatus[]>;
|
|
52
|
+
export declare function retryFailedItems(jobId: string): Promise<EmailJobStatus>;
|
|
53
|
+
export interface ProcessBatchResult {
|
|
54
|
+
jobId: string;
|
|
55
|
+
processed: number;
|
|
56
|
+
sent: number;
|
|
57
|
+
failed: number;
|
|
58
|
+
jobCompleted: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Process the next batch of pending email items.
|
|
62
|
+
*
|
|
63
|
+
* @param sendFn - Callback that renders & sends a single email.
|
|
64
|
+
* Receives the job payload + item payload. Must throw on failure.
|
|
65
|
+
* @returns Result of the batch, or null if no work to do.
|
|
66
|
+
*/
|
|
67
|
+
export declare function processNextBatch(sendFn: (jobPayload: Record<string, unknown>, itemPayload: Record<string, unknown>, recipientEmail: string, recipientUserId: string | null) => Promise<void>): Promise<ProcessBatchResult | null>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bevakning Geographic Matching Service
|
|
3
|
+
*
|
|
4
|
+
* Uses PostGIS `ST_Contains` to check if a property's coordinates fall
|
|
5
|
+
* within the bevakning's stored polygon (`geom` column on
|
|
6
|
+
* `bofrid_location_interests`). The polygon is copied from
|
|
7
|
+
* `geo.geo_regions` / `geo.geo_municipalities` / `geo.geo_localities` when the
|
|
8
|
+
* bevakning is created.
|
|
9
|
+
*
|
|
10
|
+
* Matching uses exact polygon containment (ST_Contains) — a listing must
|
|
11
|
+
* be inside the bevakning polygon. No distance buffer is applied.
|
|
12
|
+
*/
|
|
13
|
+
export interface FindMatchingOpts {
|
|
14
|
+
/** Property longitude (EPSG:4326) */
|
|
15
|
+
lon: number;
|
|
16
|
+
/** Property latitude (EPSG:4326) */
|
|
17
|
+
lat: number;
|
|
18
|
+
/** Exclude bevakningar owned by this user (the landlord) */
|
|
19
|
+
excludeUserId: string;
|
|
20
|
+
/** Only return bevakningar with emailNotifications = true */
|
|
21
|
+
emailNotificationsOnly?: boolean;
|
|
22
|
+
/** Include nationwide bevakningar (default: false — too broad for most use cases) */
|
|
23
|
+
includeNationwide?: boolean;
|
|
24
|
+
/** Exclude specific bevakning IDs (e.g. already notified) */
|
|
25
|
+
excludeBevakningIds?: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Find active bevakningar whose stored polygon contains the given point.
|
|
29
|
+
* Returns bevakning rows joined with the user's email.
|
|
30
|
+
*/
|
|
31
|
+
export declare function findMatchingBevakningar(opts: FindMatchingOpts): Promise<{
|
|
32
|
+
userEmail: string | null;
|
|
33
|
+
id: string;
|
|
34
|
+
userId: string;
|
|
35
|
+
name: string;
|
|
36
|
+
active: boolean;
|
|
37
|
+
location: unknown;
|
|
38
|
+
filters: unknown;
|
|
39
|
+
emailNotifications: boolean;
|
|
40
|
+
availableFrom: string | null;
|
|
41
|
+
furnishedPreference: string | null;
|
|
42
|
+
numberOfPeople: number | null;
|
|
43
|
+
partnerId: string | null;
|
|
44
|
+
countryCode: string;
|
|
45
|
+
locationName: string | null;
|
|
46
|
+
locationType: string | null;
|
|
47
|
+
geoSlug: string | null;
|
|
48
|
+
regionSlug: string | null;
|
|
49
|
+
municipalitySlug: string | null;
|
|
50
|
+
areaSlug: string | null;
|
|
51
|
+
isNationwide: boolean;
|
|
52
|
+
maxRent: number | null;
|
|
53
|
+
propertyTypes: string[] | null;
|
|
54
|
+
geom: string | null;
|
|
55
|
+
origin: string | null;
|
|
56
|
+
createdAt: Date;
|
|
57
|
+
updatedAt: Date;
|
|
58
|
+
}[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Count active bevakningar whose stored polygon contains the given point.
|
|
61
|
+
*/
|
|
62
|
+
export declare function countMatchingBevakningar(opts: Omit<FindMatchingOpts, "excludeBevakningIds">): Promise<number>;
|
|
63
|
+
export declare function backfillBevakningPolygons(dryRun?: boolean): Promise<{
|
|
64
|
+
lan: number;
|
|
65
|
+
kommun: number;
|
|
66
|
+
area: number;
|
|
67
|
+
}>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geo-listings service — resolves listings by geographic filter,
|
|
3
|
+
* joins with properties to get owner data, and groups by owner.
|
|
4
|
+
*
|
|
5
|
+
* Now queries listings directly via the dedicated geo columns
|
|
6
|
+
* (geo_lan, geo_kommun, geo_omrade) on bofrid_listings.
|
|
7
|
+
*/
|
|
8
|
+
export interface GeoFilter {
|
|
9
|
+
lan?: string;
|
|
10
|
+
kommun?: string;
|
|
11
|
+
tatort?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface GeoListingRow {
|
|
14
|
+
listingId: string;
|
|
15
|
+
landlordId: string | null;
|
|
16
|
+
companyId: string | null;
|
|
17
|
+
geo: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface OwnerGeoStats {
|
|
20
|
+
count: number;
|
|
21
|
+
tatorter: Set<string>;
|
|
22
|
+
municipalities: Set<string>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Query listings that match the given geographic filter.
|
|
26
|
+
* Uses the `geo` JSONB column on bofrid_listings for filtering,
|
|
27
|
+
* joined with properties for owner IDs.
|
|
28
|
+
*
|
|
29
|
+
* This is a single SQL query — no HTTP round-trip needed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveListingsByGeo(filter: GeoFilter): Promise<GeoListingRow[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Group GeoListingRows by owner type.
|
|
34
|
+
*
|
|
35
|
+
* - `"landlord"` → individual landlords (has landlordId, no companyId)
|
|
36
|
+
* - `"company"` → company-owned (has companyId)
|
|
37
|
+
*/
|
|
38
|
+
export declare function groupByOwner(rows: GeoListingRow[], ownerType: "landlord" | "company", tatort?: string): Map<string, OwnerGeoStats>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geo Service — direct Drizzle queries against the `geo.*` tables.
|
|
3
|
+
*
|
|
4
|
+
* The geo tables live in the same Supabase instance, so we query them
|
|
5
|
+
* directly via SQL rather than over HTTP.
|
|
6
|
+
*/
|
|
7
|
+
import type { GeoCenter, GeoBoundingBox } from "@bofrid/db/geo-schema";
|
|
8
|
+
export interface GeoReverseResult {
|
|
9
|
+
region: {
|
|
10
|
+
name: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
};
|
|
13
|
+
municipality: {
|
|
14
|
+
name: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
};
|
|
17
|
+
areas: Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
type: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export interface GeoAutocompleteResult {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
type: "region" | "municipality" | "tatort" | "regso" | "stadsdelsomrade";
|
|
27
|
+
slug: string;
|
|
28
|
+
/** ISO 3166-1 alpha-2 ("SE", "DK", ...). Lets the UI render flags + filter cross-market. */
|
|
29
|
+
countryCode: string;
|
|
30
|
+
municipalitySlug?: string;
|
|
31
|
+
municipalityName?: string;
|
|
32
|
+
regionSlug?: string;
|
|
33
|
+
regionName?: string;
|
|
34
|
+
center: GeoCenter | null;
|
|
35
|
+
boundingbox: GeoBoundingBox | null;
|
|
36
|
+
population?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface GeoHierarchyLan {
|
|
39
|
+
slug: string;
|
|
40
|
+
name: string;
|
|
41
|
+
kommunCount: number | null;
|
|
42
|
+
tatortCount: number | null;
|
|
43
|
+
}
|
|
44
|
+
export interface GeoHierarchyKommun {
|
|
45
|
+
type: "municipality";
|
|
46
|
+
slug: string;
|
|
47
|
+
name: string;
|
|
48
|
+
areas: Array<{
|
|
49
|
+
type: string;
|
|
50
|
+
slug: string;
|
|
51
|
+
name: string;
|
|
52
|
+
population?: number | null;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
export interface GeoEntityWithPolygon {
|
|
56
|
+
name: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
type: string;
|
|
59
|
+
center: GeoCenter | null;
|
|
60
|
+
boundingbox: GeoBoundingBox | null;
|
|
61
|
+
municipalitySlug?: string | null;
|
|
62
|
+
municipalityName?: string | null;
|
|
63
|
+
regionSlug?: string | null;
|
|
64
|
+
regionName?: string | null;
|
|
65
|
+
polygonJson: string | null;
|
|
66
|
+
population?: number | null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Reverse geocode coordinates → län + kommun + areas.
|
|
70
|
+
* Uses PostGIS `ST_Contains` against the `geom` column.
|
|
71
|
+
*/
|
|
72
|
+
export declare function reverseGeocode(lat: number, lng: number): Promise<GeoReverseResult | null>;
|
|
73
|
+
export declare function getLanBySlug(slug: string): Promise<{
|
|
74
|
+
slug: string;
|
|
75
|
+
name: string;
|
|
76
|
+
nameEn: string | null;
|
|
77
|
+
code: string;
|
|
78
|
+
countryCode: string;
|
|
79
|
+
center: GeoCenter | null;
|
|
80
|
+
boundingbox: GeoBoundingBox | null;
|
|
81
|
+
polygonJson: string | null;
|
|
82
|
+
disabled: boolean | null;
|
|
83
|
+
seo: import("@bofrid/db").GeoSeo | null;
|
|
84
|
+
content: unknown;
|
|
85
|
+
landlordContent: unknown;
|
|
86
|
+
tenantContent: unknown;
|
|
87
|
+
wikipediaData: unknown;
|
|
88
|
+
searchTokens: string[] | null;
|
|
89
|
+
osmId: number | null;
|
|
90
|
+
osmType: string | null;
|
|
91
|
+
wikidataId: string | null;
|
|
92
|
+
kommunCount: number | null;
|
|
93
|
+
tatortCount: number | null;
|
|
94
|
+
postalCodeCount: number | null;
|
|
95
|
+
population: number | null;
|
|
96
|
+
listingCount: number | null;
|
|
97
|
+
listings: unknown;
|
|
98
|
+
tenantAds: unknown;
|
|
99
|
+
createdAt: Date | null;
|
|
100
|
+
updatedAt: Date | null;
|
|
101
|
+
geom: string | null;
|
|
102
|
+
}>;
|
|
103
|
+
export declare function getKommunBySlug(slug: string): Promise<{
|
|
104
|
+
slug: string;
|
|
105
|
+
name: string;
|
|
106
|
+
code: string;
|
|
107
|
+
countryCode: string;
|
|
108
|
+
regionSlug: string;
|
|
109
|
+
regionCode: string | null;
|
|
110
|
+
regionName: string | null;
|
|
111
|
+
center: GeoCenter | null;
|
|
112
|
+
boundingbox: GeoBoundingBox | null;
|
|
113
|
+
polygonJson: string | null;
|
|
114
|
+
disabled: boolean | null;
|
|
115
|
+
seo: import("@bofrid/db").GeoSeo | null;
|
|
116
|
+
content: unknown;
|
|
117
|
+
landlordContent: unknown;
|
|
118
|
+
tenantContent: unknown;
|
|
119
|
+
wikipediaData: unknown;
|
|
120
|
+
postalCodes: unknown;
|
|
121
|
+
searchTokens: string[] | null;
|
|
122
|
+
osmId: number | null;
|
|
123
|
+
osmType: string | null;
|
|
124
|
+
wikidataId: string | null;
|
|
125
|
+
hasUniversity: boolean | null;
|
|
126
|
+
targetStudents: boolean | null;
|
|
127
|
+
universities: import("@bofrid/db").GeoUniversity[] | null;
|
|
128
|
+
areaCount: number | null;
|
|
129
|
+
tatortCount: number | null;
|
|
130
|
+
postalCodeCount: number | null;
|
|
131
|
+
population: number | null;
|
|
132
|
+
listingCount: number | null;
|
|
133
|
+
listings: unknown;
|
|
134
|
+
tenantAds: unknown;
|
|
135
|
+
createdAt: Date | null;
|
|
136
|
+
updatedAt: Date | null;
|
|
137
|
+
geom: string | null;
|
|
138
|
+
}>;
|
|
139
|
+
export declare function getAreaBySlugAndKommun(slug: string, municipalitySlug: string): Promise<{
|
|
140
|
+
id: string;
|
|
141
|
+
type: string;
|
|
142
|
+
slug: string;
|
|
143
|
+
name: string;
|
|
144
|
+
countryCode: string;
|
|
145
|
+
municipalitySlug: string | null;
|
|
146
|
+
municipalityCode: string | null;
|
|
147
|
+
municipalityName: string | null;
|
|
148
|
+
regionSlug: string | null;
|
|
149
|
+
regionCode: string | null;
|
|
150
|
+
regionName: string | null;
|
|
151
|
+
center: GeoCenter | null;
|
|
152
|
+
boundingbox: GeoBoundingBox | null;
|
|
153
|
+
polygonJson: string | null;
|
|
154
|
+
disabled: boolean | null;
|
|
155
|
+
seo: import("@bofrid/db").GeoSeo | null;
|
|
156
|
+
content: unknown;
|
|
157
|
+
landlordContent: unknown;
|
|
158
|
+
tenantContent: unknown;
|
|
159
|
+
wikipediaData: unknown;
|
|
160
|
+
famousFor: string | null;
|
|
161
|
+
highlights: string[] | null;
|
|
162
|
+
searchTokens: string[] | null;
|
|
163
|
+
tatortskod: string | null;
|
|
164
|
+
population2023: number | null;
|
|
165
|
+
population2024: number | null;
|
|
166
|
+
areaHa: string | null;
|
|
167
|
+
code: string | null;
|
|
168
|
+
source: string | null;
|
|
169
|
+
version: string | null;
|
|
170
|
+
objektidentitet: string | null;
|
|
171
|
+
areaUuid: string | null;
|
|
172
|
+
districtType: string | null;
|
|
173
|
+
osmId: number | null;
|
|
174
|
+
osmType: string | null;
|
|
175
|
+
wikidataId: string | null;
|
|
176
|
+
dataYear: number | null;
|
|
177
|
+
validFrom: Date | null;
|
|
178
|
+
validTo: Date | null;
|
|
179
|
+
hasUniversity: boolean | null;
|
|
180
|
+
targetStudents: boolean | null;
|
|
181
|
+
universities: import("@bofrid/db").GeoUniversity[] | null;
|
|
182
|
+
listingCount: number | null;
|
|
183
|
+
listings: unknown;
|
|
184
|
+
tenantAds: unknown;
|
|
185
|
+
createdAt: Date | null;
|
|
186
|
+
updatedAt: Date | null;
|
|
187
|
+
geom: string | null;
|
|
188
|
+
}>;
|
|
189
|
+
export declare function getGeoEntityWithPolygon(type: string, slug: string, municipalitySlug?: string): Promise<GeoEntityWithPolygon | null>;
|
|
190
|
+
/**
|
|
191
|
+
* Search geo entities by name prefix. Searches län, kommuner, and areas.
|
|
192
|
+
* Returns results sorted by population (largest first).
|
|
193
|
+
*/
|
|
194
|
+
export declare function autocomplete(query: string, limit?: number,
|
|
195
|
+
/**
|
|
196
|
+
* Restrict results to one country. If omitted, results are restricted to
|
|
197
|
+
* the markets currently flipped `enabled=true` on geo_countries — which is
|
|
198
|
+
* SE-only today, so backwards-compatible for existing callers.
|
|
199
|
+
*/
|
|
200
|
+
countryCode?: string): Promise<GeoAutocompleteResult[]>;
|
|
201
|
+
/** Get all 21 län with stats. */
|
|
202
|
+
export declare function getHierarchyList(): Promise<GeoHierarchyLan[]>;
|
|
203
|
+
/** Get a län's full hierarchy: kommuner → areas. */
|
|
204
|
+
export declare function getHierarchyForLan(regionSlug: string): Promise<{
|
|
205
|
+
slug: string;
|
|
206
|
+
name: string;
|
|
207
|
+
kommuner: GeoHierarchyKommun[];
|
|
208
|
+
} | null>;
|
|
209
|
+
/**
|
|
210
|
+
* Fetch ALL län hierarchies in 3 bulk queries instead of 63 individual ones.
|
|
211
|
+
* Used by /geo/hierarchy-bulk to avoid connection pool starvation.
|
|
212
|
+
*/
|
|
213
|
+
export declare function getAllHierarchies(): Promise<Array<{
|
|
214
|
+
slug: string;
|
|
215
|
+
name: string;
|
|
216
|
+
kommuner: GeoHierarchyKommun[];
|
|
217
|
+
}>>;
|
|
218
|
+
/** Count published listings in a län */
|
|
219
|
+
export declare function getLiveListingCountByLan(regionSlug: string): Promise<number>;
|
|
220
|
+
/** Count published listings in a kommun */
|
|
221
|
+
export declare function getLiveListingCountByKommun(municipalitySlug: string): Promise<number>;
|
|
222
|
+
/** Count published listings in an area within a kommun */
|
|
223
|
+
export declare function getLiveListingCountByArea(municipalitySlug: string, areaSlug: string): Promise<number>;
|
|
224
|
+
/** Count active location interests targeting a län (directly or via kommun/area within it) */
|
|
225
|
+
export declare function getActiveSeekerCountByLan(regionSlug: string): Promise<number>;
|
|
226
|
+
/** Count active location interests targeting a kommun (directly or via area within it) */
|
|
227
|
+
export declare function getActiveSeekerCountByKommun(municipalitySlug: string): Promise<number>;
|
|
228
|
+
/** Listing stats grouped by län (for /geo/listings/stats endpoint) */
|
|
229
|
+
export declare function getListingStatsByLan(): Promise<Array<{
|
|
230
|
+
slug: string;
|
|
231
|
+
name: string;
|
|
232
|
+
count: number;
|
|
233
|
+
}>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geocode Service — forward geocoding via Google Places Text Search.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the /geocoding/search route handler for reuse in
|
|
5
|
+
* the publish flow (ensurePropertyLocation) and elsewhere.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Geocode a street address to coordinates using Google Places Text Search.
|
|
9
|
+
* Biased to Sweden (lat 55–69.5, lng 10–24.5).
|
|
10
|
+
*
|
|
11
|
+
* @returns `{ lat, lon }` or `null` if the address could not be resolved.
|
|
12
|
+
*/
|
|
13
|
+
export declare function geocodeAddress(address: string): Promise<{
|
|
14
|
+
lat: number;
|
|
15
|
+
lon: number;
|
|
16
|
+
} | null>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Market Insights by Coordinates
|
|
3
|
+
*
|
|
4
|
+
* Takes (lat, lng) → reverse geocodes to area/municipality/län via PostGIS polygons
|
|
5
|
+
* → queries geo.unified_listings for market data at ALL three levels
|
|
6
|
+
* and for ALL property types.
|
|
7
|
+
*
|
|
8
|
+
* Returns a complete picture: area + municipality + län insights, each broken down
|
|
9
|
+
* by property type (apartment, house, room, all).
|
|
10
|
+
*/
|
|
11
|
+
export interface MarketStats {
|
|
12
|
+
sampleSize: number;
|
|
13
|
+
avgKvmPris: number;
|
|
14
|
+
medianKvmPris: number;
|
|
15
|
+
avgRent: number;
|
|
16
|
+
medianRent: number;
|
|
17
|
+
rentP25: number;
|
|
18
|
+
rentP75: number;
|
|
19
|
+
avgSqm: number;
|
|
20
|
+
/** Suggested rent if sqm was provided */
|
|
21
|
+
suggestedRent: number | null;
|
|
22
|
+
}
|
|
23
|
+
export interface YearlyMarketStats {
|
|
24
|
+
year: number;
|
|
25
|
+
byType: Record<string, MarketStats>;
|
|
26
|
+
}
|
|
27
|
+
export interface MarketInsightLevel {
|
|
28
|
+
level: "area" | "municipality" | "region";
|
|
29
|
+
name: string;
|
|
30
|
+
slug: string;
|
|
31
|
+
/** Extra info: area type, municipality code, etc. */
|
|
32
|
+
meta?: Record<string, string>;
|
|
33
|
+
/** Stats per property type + "all" (all time) */
|
|
34
|
+
byType: Record<string, MarketStats>;
|
|
35
|
+
/** Stats per year (only years with enough data) */
|
|
36
|
+
byYear: YearlyMarketStats[];
|
|
37
|
+
}
|
|
38
|
+
export interface CoordsMarketInsightResult {
|
|
39
|
+
lat: number;
|
|
40
|
+
lng: number;
|
|
41
|
+
/** Resolved location hierarchy */
|
|
42
|
+
location: {
|
|
43
|
+
area: {
|
|
44
|
+
name: string;
|
|
45
|
+
slug: string;
|
|
46
|
+
type: string;
|
|
47
|
+
} | null;
|
|
48
|
+
municipality: {
|
|
49
|
+
name: string;
|
|
50
|
+
slug: string;
|
|
51
|
+
} | null;
|
|
52
|
+
region: {
|
|
53
|
+
name: string;
|
|
54
|
+
slug: string;
|
|
55
|
+
} | null;
|
|
56
|
+
};
|
|
57
|
+
/** Insights at each geographic level (area, municipality, län) */
|
|
58
|
+
insights: MarketInsightLevel[];
|
|
59
|
+
}
|
|
60
|
+
export interface CoordsMarketInsightParams {
|
|
61
|
+
lat: number;
|
|
62
|
+
lng: number;
|
|
63
|
+
sqm?: number | null;
|
|
64
|
+
/** Skip yearly breakdown (default true — saves 3 queries) */
|
|
65
|
+
skipYearly?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export declare function getMarketInsightsByCoords(params: CoordsMarketInsightParams): Promise<CoordsMarketInsightResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Market Insights Service
|
|
3
|
+
*
|
|
4
|
+
* Provides real rental market data from geo.unified_listings (86k+ listings
|
|
5
|
+
* scraped from 329 Swedish rental platforms) to help landlords price competitively.
|
|
6
|
+
*
|
|
7
|
+
* Returns kvm-pris (kr/kvm/month), rent ranges, and sample sizes
|
|
8
|
+
* at the most granular available level: area → kommun → län.
|
|
9
|
+
*/
|
|
10
|
+
export interface MarketInsight {
|
|
11
|
+
/** Geographic level the data comes from */
|
|
12
|
+
level: "area" | "municipality" | "region";
|
|
13
|
+
/** Human-readable location name */
|
|
14
|
+
locationName: string;
|
|
15
|
+
/** Number of comparable listings used */
|
|
16
|
+
sampleSize: number;
|
|
17
|
+
/** Average kr/kvm/month */
|
|
18
|
+
avgKvmPris: number;
|
|
19
|
+
/** Median kr/kvm/month */
|
|
20
|
+
medianKvmPris: number;
|
|
21
|
+
/** Average monthly rent (SEK) */
|
|
22
|
+
avgRent: number;
|
|
23
|
+
/** Median monthly rent (SEK) */
|
|
24
|
+
medianRent: number;
|
|
25
|
+
/** 25th percentile rent */
|
|
26
|
+
rentP25: number;
|
|
27
|
+
/** 75th percentile rent */
|
|
28
|
+
rentP75: number;
|
|
29
|
+
/** Average size in m² */
|
|
30
|
+
avgSqm: number;
|
|
31
|
+
/** Suggested rent based on property size (avgKvmPris × sqm) */
|
|
32
|
+
suggestedRent: number | null;
|
|
33
|
+
/** Property type used for filtering */
|
|
34
|
+
propertyType: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface MarketInsightParams {
|
|
37
|
+
municipalitySlug: string;
|
|
38
|
+
areaId?: string | null;
|
|
39
|
+
regionSlug?: string | null;
|
|
40
|
+
propertyType?: string | null;
|
|
41
|
+
/** Property size in m² — used to calculate suggested rent */
|
|
42
|
+
sqm?: number | null;
|
|
43
|
+
}
|
|
44
|
+
export declare function getMarketInsights(params: MarketInsightParams): Promise<MarketInsight | null>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Market Overview Service
|
|
3
|
+
*
|
|
4
|
+
* Bulk variant of `getMarketInsights` — returns aggregated rent stats for
|
|
5
|
+
* many kommuner in one query against `geo.unified_listings` (the 86k+
|
|
6
|
+
* listings scraped from Qasa, Homii, HomeQ and Kvalster).
|
|
7
|
+
*
|
|
8
|
+
* Used by the landlord-facing /dashboard/hyresmarknad page to render a
|
|
9
|
+
* sortable table of kommuner with median rent, kr/kvm and sample size.
|
|
10
|
+
*/
|
|
11
|
+
export type PropertyTypeFilter = "apartment" | "house" | "room" | "student" | "all";
|
|
12
|
+
export interface MarketOverviewParams {
|
|
13
|
+
regionSlug?: string | null;
|
|
14
|
+
propertyType?: PropertyTypeFilter | null;
|
|
15
|
+
minSamples?: number;
|
|
16
|
+
limit?: number;
|
|
17
|
+
sortBy?: "sampleSize" | "medianRent" | "medianKvmPris";
|
|
18
|
+
}
|
|
19
|
+
export interface MarketOverviewRow {
|
|
20
|
+
municipalitySlug: string;
|
|
21
|
+
municipalityName: string;
|
|
22
|
+
regionSlug: string;
|
|
23
|
+
regionName: string;
|
|
24
|
+
sampleSize: number;
|
|
25
|
+
medianRent: number;
|
|
26
|
+
medianKvmPris: number;
|
|
27
|
+
avgSqm: number;
|
|
28
|
+
rentP25: number;
|
|
29
|
+
rentP75: number;
|
|
30
|
+
}
|
|
31
|
+
export interface MarketOverviewSwedenWide {
|
|
32
|
+
medianRent: number;
|
|
33
|
+
medianKvmPris: number;
|
|
34
|
+
avgSqm: number;
|
|
35
|
+
sampleSize: number;
|
|
36
|
+
}
|
|
37
|
+
export interface MarketOverviewResult {
|
|
38
|
+
totalListings: number;
|
|
39
|
+
swedenWide: MarketOverviewSwedenWide | null;
|
|
40
|
+
rows: MarketOverviewRow[];
|
|
41
|
+
}
|
|
42
|
+
export declare function getMarketOverview(params?: MarketOverviewParams): Promise<MarketOverviewResult>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Homii image service — fetch JPEG from Bofrid CDN and upload to Homii.
|
|
3
|
+
*
|
|
4
|
+
* Homii only accepts image/jpeg via their Cloud Run upload endpoint.
|
|
5
|
+
* We fetch the JPEG variant from our CDN (bofrid.media) to avoid needing sharp.
|
|
6
|
+
*/
|
|
7
|
+
export interface ImageUploadResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
imageUrl?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get the best JPEG URL from property image data.
|
|
14
|
+
* Prefers medium JPEG, falls back to original URL.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getJpegUrl(images: Array<{
|
|
17
|
+
url: string;
|
|
18
|
+
urls: unknown;
|
|
19
|
+
isPrimary: boolean;
|
|
20
|
+
}>): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Fetch a JPEG image from source URL and upload to Homii's Cloud Run endpoint.
|
|
23
|
+
*/
|
|
24
|
+
export declare function uploadImage(sourceUrl: string, homiiListingId: string, maxRetries?: number): Promise<ImageUploadResult>;
|