Skip to content

Commit 626620d

Browse files
authored
Merge pull request #435 from pennlabs/jordan-analytics-graph-fix
Made dining analytics graph predict from most recent increase in balance after dining plan start date
2 parents 3eef4d8 + 79ed8b0 commit 626620d

6 files changed

Lines changed: 81 additions & 9 deletions

File tree

PennMobile.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
21F70D252346AB3800CEB203 /* CampusExpressLoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F70D242346AB3400CEB203 /* CampusExpressLoginController.swift */; };
113113
21FBC240228514ED00B432D8 /* FeedAnalyticsEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21FBC23F228514ED00B432D8 /* FeedAnalyticsEngine.swift */; };
114114
21FBC242228B774E00B432D8 /* PennCashNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21FBC241228B774E00B432D8 /* PennCashNetworkManager.swift */; };
115+
425E281B28FF5EE200218F50 /* DiningPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425E281A28FF5EE200218F50 /* DiningPlan.swift */; };
115116
56D74230B1B43DAF260BCCBE /* Pods_PennMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BED8AA4945D67F0ED89FA9B0 /* Pods_PennMobile.framework */; };
116117
6C11C08026F842CF00407C04 /* HomeUpdateCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C11C07F26F842CF00407C04 /* HomeUpdateCellItem.swift */; };
117118
6C1991BB27B5C73800BBB402 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1991BA27B5C73800BBB402 /* Environment.swift */; };
@@ -464,6 +465,7 @@
464465
22D5D1D7F760F28A27C06241 /* libPods-AutomatedScreenshotUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AutomatedScreenshotUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
465466
2F8A024230F1E8D7BA9410B4 /* Pods-AutomatedScreenshotUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AutomatedScreenshotUITests.debug.xcconfig"; path = "Target Support Files/Pods-AutomatedScreenshotUITests/Pods-AutomatedScreenshotUITests.debug.xcconfig"; sourceTree = "<group>"; };
466467
407FC3CFC29C6496A17C99C2 /* Pods-AutomatedScreenshotUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AutomatedScreenshotUITests.release.xcconfig"; path = "Target Support Files/Pods-AutomatedScreenshotUITests/Pods-AutomatedScreenshotUITests.release.xcconfig"; sourceTree = "<group>"; };
468+
425E281A28FF5EE200218F50 /* DiningPlan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningPlan.swift; sourceTree = "<group>"; };
467469
6C11C07F26F842CF00407C04 /* HomeUpdateCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeUpdateCellItem.swift; sourceTree = "<group>"; };
468470
6C1991BA27B5C73800BBB402 /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = "<group>"; };
469471
6C1991BC27B5CA4D00BBB402 /* NotificationDevelopment.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NotificationDevelopment.xcconfig; sourceTree = "<group>"; };
@@ -915,6 +917,7 @@
915917
2166408D1EBADC0100746B8E /* Model */ = {
916918
isa = PBXGroup;
917919
children = (
920+
425E281A28FF5EE200218F50 /* DiningPlan.swift */,
918921
6CC88D7A27B1BFB0006896F6 /* DiningToken.swift */,
919922
E515B36227C2B97500AAE0D8 /* DiningBalance.swift */,
920923
E50676E927EE1C73009A776E /* PastDiningBalances.swift */,
@@ -2239,6 +2242,7 @@
22392242
F212BE8823B71C7100ED46A1 /* NotificationsTableViewController.swift in Sources */,
22402243
6CC88D6527B1BF51006896F6 /* PredictionsGraphView+AxisLabels.swift in Sources */,
22412244
21F5F8F320538A90005B143F /* HomeFlingCell.swift in Sources */,
2245+
425E281B28FF5EE200218F50 /* DiningPlan.swift in Sources */,
22422246
212B8355222A31B500F835D6 /* HomePostCellItem.swift in Sources */,
22432247
97E79E062100DA1200D3D606 /* BuildingMapCell.swift in Sources */,
22442248
6CC88D7727B1BF51006896F6 /* DiningAnalyticsView.swift in Sources */,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// DiningPlan.swift
3+
// PennMobile
4+
//
5+
// Created by Jordan H on 10/18/22.
6+
// Copyright © 2022 PennLabs. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
struct DiningPlan: Codable {
12+
let name: String
13+
let description: String
14+
let start_date: Date
15+
let end_date: Date
16+
let signup_date: Date
17+
let cost: String
18+
let dining_dollars: String
19+
let total_visits: Int
20+
}

PennMobile/Dining/Networking + Cache/DiningAPI.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,25 @@ extension DiningAPI {
137137
task.resume()
138138
}
139139
}
140+
141+
// MARK: Current Dining Plan Start Date
142+
extension DiningAPI {
143+
func getDiningPlanStartDate(diningToken: String) async -> Result<Date, NetworkingError> {
144+
let url = URL(string: "https://prod.campusexpress.upenn.edu/api/v1/dining/currentPlan")!
145+
var request = URLRequest(url: url)
146+
request.httpMethod = "GET"
147+
request.setValue(diningToken, forHTTPHeaderField: "x-authorization")
148+
guard let (data, response) = try? await URLSession.shared.data(for: request), let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
149+
return .failure(.serverError)
150+
}
151+
let decoder = JSONDecoder()
152+
let dateFormatter = DateFormatter()
153+
dateFormatter.dateFormat = "yyyy-MM-dd"
154+
decoder.dateDecodingStrategy = .formatted(dateFormatter)
155+
if let plan = try? decoder.decode(DiningPlan.self, from: data) {
156+
return .success(plan.start_date)
157+
} else {
158+
return .failure(.parsingError)
159+
}
160+
}
161+
}

PennMobile/Dining/SwiftUI/DiningAnalyticsView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ struct DiningAnalyticsView: View {
5757
}
5858
.padding()
5959
}
60-
.onAppear {
60+
.task {
6161
guard Account.isLoggedIn, KeychainAccessible.instance.getDiningToken() != nil, let diningExpiration = UserDefaults.standard.getDiningTokenExpiration(), Date() <= diningExpiration else {
6262
showMissingDiningTokenAlert = true
6363
return
6464
}
65-
diningAnalyticsViewModel.refresh()
65+
await diningAnalyticsViewModel.refresh()
6666
}
6767
.alert(isPresented: $showMissingDiningTokenAlert) {
6868
showCorrectAlert()

PennMobile/Dining/SwiftUI/DiningAnalyticsViewModel.swift

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class DiningAnalyticsViewModel: ObservableObject {
4747
Storage.remove(DiningAnalyticsViewModel.swipeHistoryDirectory, from: .documents)
4848
}
4949
}
50-
func refresh() {
50+
func refresh() async {
5151
guard let diningToken = KeychainAccessible.instance.getDiningToken() else {
5252
return
5353
}
@@ -56,6 +56,7 @@ class DiningAnalyticsViewModel: ObservableObject {
5656
startDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)!
5757
}
5858
let startDateStr = formatter.string(from: startDate)
59+
let planStartDateResult = await DiningAPI.instance.getDiningPlanStartDate(diningToken: diningToken)
5960
DiningAPI.instance.getPastDiningBalances(diningToken: diningToken, startDate: startDateStr) { (balances) in
6061
guard let balances = balances else {
6162
return
@@ -76,19 +77,41 @@ class DiningAnalyticsViewModel: ObservableObject {
7677
let maxSwipeBalance = (self.swipeHistory.max { $0.balance < $1.balance }) else {
7778
return
7879
}
79-
let dollarPredictions = self.getPredictions(firstBalance: maxDollarBalance, lastBalance: lastDollarBalance)
80+
// If no dining plan found, refresh will return, these are just placeholders
81+
var startDollarBalance = maxDollarBalance
82+
var startSwipeBalance = maxSwipeBalance
83+
switch planStartDateResult {
84+
case .failure:
85+
return
86+
case .success(let planStartDate):
87+
// If dining plan found, start prediction from the date dining plan started
88+
startDollarBalance = (self.dollarHistory.first { $0.date == planStartDate }) ?? startDollarBalance
89+
startSwipeBalance = (self.swipeHistory.first { $0.date == planStartDate }) ?? startSwipeBalance
90+
// However, it's possible that people recharged dining dollars (swipes maybe?), and if so, predict from this date (most recent increase)
91+
for (i, day) in self.dollarHistory.enumerated() {
92+
if i != 0 && day.date > planStartDate && day.balance > self.dollarHistory[i - 1].balance {
93+
startDollarBalance = day
94+
}
95+
}
96+
for (i, day) in self.swipeHistory.enumerated() {
97+
if i != 0 && day.date > planStartDate && day.balance > self.swipeHistory[i - 1].balance {
98+
startSwipeBalance = day
99+
}
100+
}
101+
}
102+
let dollarPredictions = self.getPredictions(firstBalance: startDollarBalance, lastBalance: lastDollarBalance, maxBalance: maxDollarBalance)
80103
self.dollarSlope = dollarPredictions.slope
81104
self.dollarPredictedZeroDate = dollarPredictions.predictedZeroDate
82105
self.predictedDollarSemesterEndBalance = dollarPredictions.predictedEndBalance
83106
self.dollarAxisLabel = self.getAxisLabelsYX(from: self.dollarHistory)
84-
let swipePredictions = self.getPredictions(firstBalance: maxSwipeBalance, lastBalance: lastSwipeBalance)
107+
let swipePredictions = self.getPredictions(firstBalance: startSwipeBalance, lastBalance: lastSwipeBalance, maxBalance: maxSwipeBalance)
85108
self.swipeSlope = swipePredictions.slope
86109
self.swipesPredictedZeroDate = swipePredictions.predictedZeroDate
87110
self.predictedSwipesSemesterEndBalance = swipePredictions.predictedEndBalance
88111
self.swipeAxisLabel = self.getAxisLabelsYX(from: self.swipeHistory)
89112
}
90113
}
91-
func getPredictions(firstBalance: DiningAnalyticsBalance, lastBalance: DiningAnalyticsBalance) -> (slope: Double, predictedZeroDate: Date, predictedEndBalance: Double) {
114+
func getPredictions(firstBalance: DiningAnalyticsBalance, lastBalance: DiningAnalyticsBalance, maxBalance: DiningAnalyticsBalance) -> (slope: Double, predictedZeroDate: Date, predictedEndBalance: Double) {
92115
if firstBalance.date == lastBalance.date || firstBalance.balance == lastBalance.balance {
93116
let zeroDate = Calendar.current.date(byAdding: .day, value: 1, to: Date.endOfSemester)!
94117
return (Double(0.0), zeroDate, lastBalance.balance)
@@ -97,10 +120,11 @@ class DiningAnalyticsViewModel: ObservableObject {
97120
var slope = self.getSlope(firstBalance: firstBalance, lastBalance: lastBalance)
98121
let zeroDate = self.predictZeroDate(firstBalance: firstBalance, lastBalance: lastBalance, slope: slope)
99122
let endBalance = self.predictSemesterEndBalance(firstBalance: firstBalance, lastBalance: lastBalance, slope: slope)
100-
let fullSemester = firstBalance.date.distance(to: Date.endOfSemester)
123+
let fullSemester = Date.startOfSemester.distance(to: Date.endOfSemester)
101124
let fullZeroDistance = firstBalance.date.distance(to: zeroDate)
102125
let deltaX = fullZeroDistance / fullSemester
103-
slope = -1 / deltaX // Resetting slope to different value for graph format
126+
let deltaY = firstBalance.balance / maxBalance.balance
127+
slope = -deltaY / deltaX // Resetting slope to different value for graph format
104128
return (slope, zeroDate, endBalance)
105129
}
106130
}

PennMobile/Dining/SwiftUI/DiningLoginViewSwiftUI.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ struct DiningLoginViewSwiftUI: UIViewControllerRepresentable {
3333
func dismissDiningLoginController() {
3434
parent.presentationMode.wrappedValue.dismiss()
3535
DiningViewModelSwiftUI.instance.refreshBalance()
36-
parent.diningAnalyticsViewModel.refresh()
36+
Task.init() {
37+
await parent.diningAnalyticsViewModel.refresh()
38+
}
3739
}
3840
}
3941

0 commit comments

Comments
 (0)